オブジェクト指向の三大要素、それは理解を難しくする罠

プログラミング
スポンサーリンク

対象読者

  • オブジェクト指向は難しい、と感じている方
  • オブジェクト指向言語を使っても恩恵を感じられない方

結論

巷では、オブジェクト指向プログラミングを特徴づける要素として以下の3つが挙げられる。

  • カプセル化
  • 継承
  • ポリモーフィズム

これらをまとめて、オブジェクト指向の三大要素と呼ぶ。

現在の主要なプログラミング言語において、オブジェクト指向の理解は必須である。その入門というか基礎としてこれら三要素を教える記事や書籍も多い。

しかし、これらはオブジェクト指向を使いこなし、高品質なソースコードを作るための土台となるどころか、むしろオブジェクト指向の理解を妨げる誤解を与えかねない

本記事では、オブジェクト指向の三大要素はオブジェクト指向を特徴づける要素ではないことを解説する。

まずは、カプセル化とは非オブジェクト指向でも使われる技術であり、プログラミング全般の基礎であることを確認する。

カプセル化はオブジェクト指向に限らず重要

よくある解説において、カプセル化は次の2点から説明される。すなわち、カプセル化とは、

  1. プロパティ(クラスが持つメンバ変数)をインスタンスの外部から操作できないようにアクセス修飾子をprivateとして定義することである。
  2. プロパティへのアクセスはgetter/setterというpublicなメソッドを介することによって制約を与えるべし。なぜなら、インスタンスの外部から好き勝手にプロパティの値をいじる(値の更新や取得)と、意図しないデータ操作が生じ、エラーの温床になる可能性があるからだ。

上記の解説は間違っている、というかプロパティのカプセル化の一例を示しているに過ぎない。後述するが、ポリモーフィズムもカプセル化の一例である。

カプセル化とは、隠すこと全般を意味する。隠すとは、外部との依存関係を断つことだ。外部から見えなくする、アクセスできなくする、などと言い換えることもできる。

カプセル化とは抽象化でもある。抽象化とは、些末な要素を無視する(隠す)ことで本質を浮き彫りにすることだからだ。モデリングは抽象化によって物事をシンプルに説明可能にすることだ。

といっても漠然としていて良く分からないので、オブジェクト指向の前身である手続き型プログラミングでもカプセル化が使われている例を次に示す。

例えば、関数スコープ内でのみ使用される変数をローカル変数という。これは関数内部でのみ使用され、関数が値を返した後は捨てられる情報だ。つまり、外部(関数の呼び出す側)からはどんなローカル変数が存在するか知ることはできない。ローカル変数は関数内にカプセル化されている

同様に、関数は処理の詳細をカプセル化する。関数内部でローカル変数が使われているのか、またはロジックにif文やfor文が使われているのか等は外部からは知ることができない。関数はその名前と入出力のみを公開する仕組みであり、処理の詳細に溺れることなくシンプルに意図を伝えるような高級なソースコードを作ることができる(関数の重要性ついての詳細はこちら)。

また、詳細を隠蔽することによって、外部との依存関係がなくなる。これにより変更の影響範囲が限定され、安心して修正ができるようになる(保守性の向上)。

このように、カプセル化はオブジェクト指向を特徴づける固有の要素ではなく、綺麗なソースコードを書くための汎用的かつ基礎的な技術なのだ。

差分プログラミング;継承の乱用は保守性を下げる原因

次は「継承がオブジェクト指向を特徴づける」という先入観の弊害を記す。

ショボい入門書では、継承についてのメリットが次のように説明される。すなわち「継承は、親クラス(基底クラス、スーパークラスとも)で定義したプロパティやメソッドを子クラス(具象クラス、派生クラス、サブクラスとも)で再利用できるため、コーディングの効率を高めることができる」と。

しかし、再利用の技術はオブジェクト指向以前に既にあった。そう、関数だ。処理を再利用したいのなら、別に関数で事足りるはずだ。継承による再利用がオブジェクト指向の根幹の1つだとしたら、手続き型に対するオブジェクト指向の優位性はどこにあるのだろうか。この問いに答えれない方は継承の乱用という悪の道にハマるかもしれない。

「継承によるメソッドの再利用によって、コーディングが爆速になるじゃん!ビバ、オブジェクト指向!」という考え方は差分プログラミングと呼ばれる。継承による再利用は、既存メソッドからの追加分や修正分という差分のみを書き足すことによってコードの変更を可能にしたからだ。

オブジェクト指向が主流でなかった昔、差分プログラミングはオブジェクト指向の良さを宣伝する謳い文句として使われた。その甲斐もあってか、オブジェクト指向言語が普及した。プログラマへ楽園がもたらされるはずだった。しかし、差分プログラミングによって継承が乱用されることで、以下のような問題が生じた。

  • オーバーライドの多用はそのメソッドの処理内容を理解を難しくした;そのメソッドを理解するためには、複数の親クラスの定義を追わなければならなくなった。
  • 親クラスの修正の影響を受ける子クラスや孫・子孫クラスの数が多くなって、保守性が下がった。

このように、差分プログラムはカプセル化を破壊し、プログラマにユートピアではなくディストピアをもたらした。ひょっとしたら、無駄に複雑なオブジェクト指向よりも手続き型の方が良かったのではないか、とすら思わせるほどに。

いや、そんなはずはない。オブジェクト指向は手続き型の弱点を克服するために生まれた技術だからだ。問題の原因は継承自体ではなく、継承の乱用にある。

オブジェクト指向の最も基本的な原則として「単一責任の原則」がある。要は、1つのクラスには1つの役割のみを持たせるべし、という原則だ。単に再利用をしたいがための継承は、このような責任分割を侵し、低凝集・高結合な低品質なソースコードへと至る。継承 = 再利用 という先入観にはこのようなリスクがある。

ポリモーフィズムとは具象クラスのカプセル化に過ぎない

最後の要素はポリモーフィズム(多態性)だ。これは継承とセットで説明されることが多い。

良くある動物クラスの例では次のように説明される。すなわち、「barkメソッドを持つ動物クラスを継承したCatクラスとDogクラスがあって、それらのインスタンスはどのクラス由来か意識せずともbarkメソッドが呼び出せる。」みたいな感じだ。

ポリモーフィズムは、同じシグニチャ(名前、引数、返り値の型)のメソッドを持つクラスのインスタンスならば、どのクラスかを区別しなくても同様にそのプロパティやメソッドを参照できることをいう。これを同じインターフェースを持つともいう。規格さえ同じならば、どこの誰がどのように製造した部品でも構わない、という標準化の考え方に似ている。

ポリモーフィズムが意味するのは、具象クラスが実際に何であるか、という詳細は気にしなくていい、ということだ。上の例では、どの具象クラスかは知らんけど、barkメソッドを持つことは分かっている。というか、barkメソッドを持っていること以外は興味ない、ということだ。これは具象クラスのカプセル化に他ならない。ポリモーフィズムはカプセル化の一例なのだ。

呼び出し側からはどの具象クラスかは分からない、ということは言い換えると「他の具象クラスと差し替えても、同様に動作させることができる」ということだ。すなわち、同じインターフェースを持つ具象クラスによって差し替えるだけで、処理内容を変更することができることを意味する。このようにポリモーフィズムによって、拡張性を向上することができる。

しかし、ポリモーフィズムはカプセル化の一部であり、オブジェクト指向の三大要素として、カプセル化と同レベルで並べられていることには違和感がある

オブジェクト指向の三大要素は独立・直交した概念ではない

以上のように、オブジェクト指向の三大要素はそれぞれがオブジェクト指向を特徴づける要素ではないことが分かった。

さらには、三大要素はそれぞれ関連していたり、部分の関係だったりする。再利用のためだけに継承を多用することは保守性を下げるし、ポリモーフィズムはカプセル化の一例であった。なぜ、カプセル化、継承、ポリモーフィズムがオブジェクト指向の三大要素などと呼ばれ、同じレベル感で並べられるのか分からないし、それは不適切ですらある。

図1.オブジェクト指向の三大要素の構造

では、オブジェクト指向を有効に使いこなすために重要なもの、つまりオブジェクト指向を特徴づけるものは何だろうか。それは「単一責任の原則」だが、それはまた別の機会に述べようと思う。

参考

  1. オブジェクト指向のこころ [アラン・シャロウェイ]

コメント

タイトルとURLをコピーしました