ソフトウェア原則[1] - OCP(Open-Close Principle) と云ふ記事に「(引用者註: プログラムをオブジェクト指向で書き直すことによって) 修正の論理を、追加の論理に変換している」と書いてあったのですがそれは公平なものの見方ではないと思ったのでここに記しておきます。
開かれてゐると云ふことの真実
元記事にある C++ で書かれた例では、 Shape 親クラスに Circle 子クラスを追加しても既存の Point 子クラスや Line 子クラスの実装を修正する必要はないことが示されてゐます。つまり、Shape クラスは新たな図形の種類を表す子クラスの追加に対して開かれてゐるといふことですね。
しかしこの C++ の例は、図形に対する操作の追加に対しては開かれてゐません。例へば、既にある draw (描画する) といふ操作に加へて translate (平行移動する) といふ操作を足したいと思ったとします。そのためには、Shape 親クラスに virtual void translate(int x, int y) = 0;
の様なメソッドを足す必要がありませう。そして、Point や Line など全ての子クラスでこのメソッドの実装を追加しなくてはいけません。機能を追加するために既存のコードを修正しなければならなくなってゐるので、これは開放閉鎖原則を満たしてゐるとは言へません。
一方、元記事にある C で書かれた例では、操作を足すのは簡単です。 void translate(Shape* shape);
の様な関数を足せば済みます。この時、既存の Shape 型や draw 関数を修正する必要はありません。つまり、C の例は新たな操作の追加に対して開かれてゐます。
要するに、ここでは「追加に対して開かれてゐる」ことの方向性が二種類あるのです。
- 図形の種類の追加に対して開かれてゐる
- 図形に対する操作の追加に対して開かれてゐる
元記事の C の例は、前者を満たしてゐませんが後者を満たしてゐます。逆に C++ の例は、前者を満たしてゐますが後者は満たしてゐません。
果たしてこれで「オブジェクト指向万歳」と手放しで喜んでいいのでせうか。
より開かれた定義
では図形の種類の追加に対しても操作の追加に対しても開かれてゐるプログラムは書けないのでせうか。そのためには、各図形の定義と操作の定義をばらす必要があります。先づ、各図形を単純な構造体として定義します。
struct Point {
int x, y;
};
struct Line {
Point p1, p2;
};
操作の追加に対して開かれてゐる様にするために、各図形が持つ座標等の値は private ではなく public にしておく必要があります。(インスタンス変数そのものを public にする代はりに、インスタンス変数への参照を返す public なゲッターメソッドを用意するのでも構ひません。本質的には同じことです。)
図形に対する操作は、Shape の場合と同様に抽象メソッドと子クラスでのオーバーライドによって定義できます。ただし、Point や Line クラスにメソッドを追加するのではなくて、新しい子クラスとして実装します。
class Drawable {
public:
virtual void draw() = 0;
};
class DrawablePoint : public Drawable, public Point {
public:
void draw() { /* draw point... */ }
};
class DrawableLine : public Drawable, public Line {
public:
void draw() { /* draw line... */ }
};
(ここでは多重継承を使ひましたが、継承ではなく合成を使ったり、あるいは値そのものではなく参照やポインタを持つ様な設計もあり得ます。)
これで、Point や Line の定義を修正することなく draw といふ操作を追加することができました。さらに translate を追加するのも同様にできます。Point や Line が操作の追加に対して開かれてゐるといふことですね。
そして Point や Line は新しい図形の種類の追加に対しても開かれてゐます。例へば Circle クラスや DrawableCircle クラスを足せばよいのです。既存の Point や DrawableLine を修正することなくできます。
ここでのポイントは、図形(が持つ座標などの値)の定義と操作の定義を別のクラスに分けたことでした。これは残念ながらオブジェクト指向の「値とそれに関連する操作の定義を一つのクラスにカプセル化する」といふ思想とは正反対のやり方です。カプセル化によって値と操作を閉ぢ籠めたことが逆に仇となってしまったのです。
Expression problem
既存の実装を弄らずに機能を追加する方法論は expression problem と呼ばれる様です (とても分かりにくい名前ですね。残念ながら定訳は無い様です)。Haskell で同様の問題に取り組む例が The Expression Problem - maoeのブログ と云ふ記事にあります。
この記事を読むとわかりますが、関数型プログラミングならば expression problem が容易に解決できると言ふわけではありません。元記事の C の例では構造体と共用体によって図形の型を定義してゐましたが、これは関数型プログラミングで一般的に使はれる代数的データ型に相当します。従って、代数的データ型を使って普通に関数型プログラミングをすると C の例と同じく図形の種類の追加に対して開いてゐないプログラムになります。
余談
元記事には "open-close principle" と書かれてゐるのですが正しくは "open/closed principle" です。