Edited at

オブジェクト指向推進派による「オブジェクト指向に騙されるな」

すみません、リライト中に誤って、「更新(アップロード)」してしまいました。

更新中ですので、また今度のアクセスをお願い致します。

今後、注意します。

あと、2か所図を入れて、一番下のパラグラフを削除、推敲、、、が続きます。


想定する読み手

アプリケーション開発をしているソフトウェア設計者/プログラマを想定しています。

基本的には、C# C++ Cのメーカー系の人を想定していますが、Javaのエンプラ系の人でも参考になるかも知れません。

ソフトウェア開発者の中には、

「バグを見つけて修正するとその影響範囲が読み切れない」状況に直面している方々が多いと思います。

私自身の個人的な経験からですが、そのようなことで大変な思いをしている人に出会ったことは数知れません。

その中でも、多い事例の一つが、継承の誤った使い方によるものです。

逆に、洗練されたライブラリなどを設計しているソフトウェア設計者/スーパープログラマは、この記事の読み手としては想定していません。

洗練されたライブラリやフレームワークを設計しているソフトウェア開発者/スーパープログラマは、

設計上の意図を明確に持ち、抽象度の切り替えを行いながら、設計し、継承を使用していると考えています。

たまたま、書籍『EffectiveC# 6.0/7.0』が手元にありますが、

これを開いてみても、継承を用いた洗練された設計は、美しいとさえ思います。

開発の中心になれているようなソフトウェア設計者/スーパープログラマには、

むしろ、スーパープログラマではない一般プログラマのことを考えてもらうという意味で、この記事を読んで欲しいです。


この記事の背景

かつて、私がソフトウェア開発に携わり始めたころ、世の中は、オブジェクト指向開発の導入最盛期でした。

日本でもオブジェクト指向開発プログラミング言語が、一部のマニアな人たちだけではなく、

一般に広まってきたときでした。

当時の私の部署でも、


  • クラスを使うことでソフトウェアの再利用性が高まる

  • 継承で差分開発がしやすくなる

  • getter setterで、情報隠ぺいが進み、疎結合になっていく

などと、今までの泥沼開発から抜け出す銀の弾丸のように謳われて、VisualStudioの開発環境が導入された記憶があります。

しかし、その顛末はご存知の通り、ほとんどの開発現場では、泥沼開発から抜け出すことができず、

「オブジェクト指向もダメだった」

と、苦しみ続ける人たちを目にしていました。

むしろ、新しい開発方法論やクラスライブラリも利用しなくてはならなくなったために、

ソフトウェア開発者の負担はより厳しくなり、うつ病になった人も複数名、見てきました。

いまだに、オブジェクト指向開発と言えば、「継承」は、三種の神器の一つみたいに言われていますが、

アプリケーションを開発している人、

つまり、汎用的なライブラリなどではなく、そのソフトウェア独自の処理を作りこんでいる人たちに向けている人たちには、

こう言いたいのです。

「基本的に継承は使うな」

.... 大人数の開発では、継承は使うな。または、1,2階層までと制限をかける。

.... スーパープログラマでないなら、継承を使うのは、一度ソフトウェア開発が終わってからの方が、良い。

継承の使い方の誤解の最たるものが、

「継承は共通処理の括りだしのために使う」という考えかたです。

この誤解を解くために、一つ一つ話を進めます。


継承と汎化と特化の違い

そもそもですが、継承と汎化と特化を区別しているでしょうか?

「継承(Inheritance)」は、

オブジェクト指向プログラミング言語において、クラスの変数やメソッドを引き継ぐことです。

つまり、継承は「プログラミング言語が用意してくれている、”引継ぎ”」のことです。

「汎化(Generalization)」「特化」は、

概念同士の上位下位の関係(一般的な概念と特殊な概念の関係)のことです。

つまり、「汎化」「特化」は「概念としての上位下位の関係性を表す言葉」です。

「汎化」はより一般的な概念を抽出することであり、

「特化」はより特殊な概念を抽出することです。

「汎化」と「特化」とは、概念の関係性を表す考えかたであるため、

一般的な側や特殊な側の片側の立場からのみ成立してはいません。

ただ関係性を見ているだけで、その関係性を一般的な側から特殊な側を見ればそれを「特化」と呼び、

特殊な側から一般的な側をみればそれを「汎化」と呼ぶだけです。

これに比べると「継承」は、必ず「引き継ぐ」関係性のことを示します。

「汎化」と「特化」でいえば、「特化」に該当します。

UML仕様書のクラス図の項目を読んでゆくと、「継承」ではなく「汎化」という言葉が使われています。

「汎化」「特化」はクラスの関係性であり、

「継承」は「引継ぎ」を実現するプログラミング言語の機能、ということになります。

モノづくりに大切なことですが、「作りたいものそのもの」と「手段」とでは、

「作りたいものそのもの」の方が大切です。

どんなに「手段」が優れていても、「作りたいもの」を描けなければ、作りたいものを手に入れることはできません。

そして、「継承」は「汎化」や「特化」を実現する「手段」であり、

「作りたいもの」の理解の一部に「汎化」「特化」が含まれていることがある。

ゆえに、「継承」という手段を有益に使うためには、「汎化」「特化」が存在するかを知ることの方が大切なのです。


継承は、結合度を上げる

誤解のもとになっている背景として、継承が絶対的に良いものだという信仰を持っている場合があります。

継承を使えば開発が分かりやすくなると言われたら、実務経験の長い人はどこかに違和感を感じていると思うのですが、

出版されている書籍を開けば、必ずと言っていい程「継承」と書いてあるので、若い人が無条件に継承を信仰してしまうのも仕方ないと思います。

さて、左が「継承」と、右が「委譲」です。

image.png

オブジェクト指向の特徴を使えば疎結合が進む、という単純な考え方をしているだけならば、それは単なる信仰です。

クラス1とクラス2の共通処理を、クラス0にまとめるとします。

「継承」と「委譲」を比べてみると、

変更箇所が1点に集まっている ⇒ 

 いずれも同じ

コードの見通しの良さ ⇒ 

 委譲は、より詳細な処理はクラス0に抽出されているため、

 クラス1やクラス2の中に詳細なコードがなく見通しが良く、

 クラス0の処理の呼び出しを追うことは容易。

 継承は、クラス0内のprotectedやpublicな変数に、

 クラス0・クラス1・クラス2のいずれもがより密にアクセスするなど、可読性に欠けやすい。

 もちろん、委譲でも継承と同等に密結合さえることはできますが、

 そもそも、委譲はクラス1とクラス2とクラス0は、別々のオブジェクトという考えかたで、

 継承は、クラス0とクラス1は一体のもの、クラス0とクラス2は一体のものという考えかたなので、

 継承の方が密なのは当たり前です。

 むしろ、継承とはそのような技法なのです。

「継承」は、

「より下位の概念で増加した特性」から「より上位の概念の特性」を疎にする点では、疎な関係を作り出しますが、

「より上位の概念」自体は「より下位の概念」の特性を引き継ぐのですから、関係が密で当たり前です。

「継承」は、ある意味でクラス間の関係を疎にしますが、別の意味ではクラス間の関係をより密にするのです。

もし、プラスとマイナスの両側面を見ずに「継承」が良いものだと信じていたのであれば、マイナスの側面も見てトレードオフを考えましょう。

クラス同士の関係性が、適切な「汎化」関係であったとしても、「継承」でなく「委譲」を使う選択肢もあります。

オブジェクト指向プログラミング言語が、「汎化」の実装手段を用意してくれていることは歓迎すべきだと思いますが、

見通しが悪いと困る場合は、使い分ければ良いのです。


「共通処理」と「抽象的な視点での処理」の不分別

継承をうまく使っている人は、

「抽象的な視点から見たときに共通する処理の流れや骨組みを括りだす」という意味で

「共通処理を括りだす」と表現することがあるようですが、

継承をうまく使っていない人は、

これを

「サブルーチンを括りだす」と解釈していることが多いようです。

そもそも、

「サブルーチンを括りだす」ことと

「抽象的な視点から見たときに共通する処理の流れや骨組みを括りだす」

では、意味が違いすぎます。

「サブルーチンを括りだす」という考え方は、

前述した「汎化」「特化」とは、まったく適合しません。

「サブルーチンを括りだす」のであれば「継承」ではなく「委譲」を使うべきなのです。


継承っぽい作業には2種類ある

オブジェクト指向分析の段階では、

アプリケーションとして扱う概念を特定し、その関係性も理解します。

この理解の中では、抽象度の異なる概念が出てきてしまったときに、

その関係性を「汎化」で整理します。

[図...E/ヨードン?引用]

これにより抽出された概念は、クラスになります。

このときに整理された「汎化」の関係で結ばれた、より抽象度の高い側が、いわゆるフレームワークになる可能性があります。

分析で見つかった「汎化」関係の実現手段に「継承」を使うことは、正論だと考えます。

オブジェクト指向設計の段階では、

通常はコードの仕組み仕掛けの再利用を目的として、

継承を使います。

これにもさらに2種類に分けて考えられます。

一つ目は、フレームワーク的なものです。

GofデザインパターンのTemplate Methodパターンのようなものです。

「汎化」の説明にも「フレームワーク」と書きましたが、

分析段階と設計段階の「フレームワーク」の違いは、意図的であるかどうかです。

分析から生まれたフレームワークは、それとしての一般的な概念と特殊な概念が元から存在しています。

設計で生み出すフレームワークは、それとしての一般的な概念は元からは存在していませんが、

設計者が再利用可能な枠組みとして一般的な概念を意図的に創造しています。

[図 .. Gof?引用]

二つ目は、ライブラリ的なものです。

例えば、イベント通知の仕組み仕掛けのようなものです。

イベント通知という概念は、抽象的な概念として成立しています。

しかし、通知する具体的なパラメータについては、一般化できません。

すると、その部分だけを「継承」を使って実装させる、という方法が考えられます。

抽象的な概念として成立しているから「継承」で実現すべきだとは考えません。

先述の通り、「サブルーチンを括りだす」的な使い方は、「継承」の使い方ではないからです。

あくまで、「抽象的な視点から見たときに共通する処理の流れや骨組みを括りだす」

用に「継承」を使うことが推奨されます。

さて、多くのソフトウェア設計者/プログラマは、

ここまでに挙げた「継承」を使う場面を、どれだけ担当するでしょうか?

「分析」に関わることはあまり多くないでしょう。

「設計」段階でのフレームワーク的なもの、については、

変化の多いアプリケーション開発の世界では、抽象を抽出すること自体がむずかしく、下手に抽象を抽出すれば、

そのあとに密結合による被害が待ち構えていますから、あまり手を出すべきところではありません。

手を出すのであれば、アプリケーション開発がひと段落終わって、将来的なシステム拡張の見通しが付いていたならば、、、

といったところまで来ていれば、良いかも知れません。

同じく「設計」段階でのライブラリ的なもの、については、

関わることはあると思いますが、上述の指摘通り、「継承」を使うべきならば「継承」を使うが、

サブルーチンを括りだすような目的では「継承」ではなく「委譲」を使うなど、

「継承」信仰せずに、落ち着いて手段を選択することが重要です。

それに、ライブラリ的なものの場合は、マイクロソフトから提供されているものも多いですから、

わざわざ自分で作る機会も減ってきているかと思います。


ここまで見てゆくと


  • 「継承」は、「抽象的な概念が成立している段階」でのみ使用可能

  • 「抽象的な概念が成立していない段階での使用」や「サブルーチンの括りだしを目的とした使用」では、密結合を推進してしまう

ですから、「継承」を使うソフトウェア設計者/プログラマは、


  • 分析モデリングができる人

  • 抽象的な概念のないところから、抽象的な視点から見たときに共通する処理の流れや骨組みを括りだす、ことのできる人

  • 一般的な抽象概念に対し、抽象的な視点から見たときに共通する処理の流れや骨組みを括りだす、ことのできる人

でなければ、「継承」の密結合に飲み込まれてしまうのだと思います。

ゆえに、


  • 上記のような技能のない人は、むしろ「継承」を使うべきでない

  • 概念上の汎化関係ではない「サブルーチンの括りだし」に「継承」を使うべきでない

という点で、「継承」は、アプリケーションの開発では、無理をして使うものではないと考えています。

そもそも、「継承」を使うべきでない場面で「継承」を使わなければ、

「継承による密結合」によって、「バグ修正の影響がどこまで及ぶか分からない」ことにはならないのです。

これって、オブジェクト指向のせいではなく、オブジェクト指向の「継承」に対する不理解によるものですよね???


継承に惑わされるな

私は、基本的にオブジェクト指向開発で継承を推奨しません。

せいぜい、完成してしまったシステムに対して、

差分開発版のシステムを作るときにはじめて導入するくらいで良いと考えています。

継承には、分析による継承と、設計による継承がありますが、

多くの場合、初めから後者を使っているケースが大半です。

分析による継承とは、要求分析上で概念を整理したときに、

抽象度の異なる視点からオブジェクトを扱う要求が混じりあっているときにのみ発生します。

(何が何だかでしょうから、図示が欲しいですね。)

設計による継承とは、そもそも作るコードが似通っているからと、

共通項を括りだして、ベースクラスを抽出するアレです。

これは、括りだしを行った時点での知見のみで継承構造を作るため、

開発が進むにつれて、共通項の理解が今一つだったり、派生クラス側に新たな要素が加わることで共通項が変わったりします。

その結果、継承という密結合により、変更が思わぬクラスへ影響してしまいます。

(ここにも図が欲しい)

これは、嬉しくない継承の使い方だと、私は考えます。

私にとって継承とは、

視点の結合方法です。

変化に対応するアジャイル(俊敏)な開発をしたければ、

継承は、視点の結合方法として使うべきで、

継承を、共通項の括りだしに使うべきではありません。

共通項の括りだしを意図して継承を使うと、

開発が進むほど、面倒が待っています。

視点の結合方法として使うと、分離して扱いたいことを分離したまま扱えます。

前者の継承は、本来推奨されるものだと考えます。

抽象度が異なるそれぞれの視点から、オブジェクトを操作するだけのことで、

その場だけの設計によるベースクラス抽出ではなく、

適切な分析によって導き出された結果なので、

経験上は、面倒なことにはなりません(なっていません)。

抽象度の視点が変化したら、それに合わせて変更するだけのことで、

割合に安全に変化についていけます。

⇒ リライトしないとなんだかですね...m(_ _)m