はじめに...
...構成、継承、コードはありませんでした。
そして、コードは不器用で、繰り返しがあり、分離できず、悲惨で、冗長で使い尽くされていました。
コードの再利用の主なツールは、コピーペーストでした。 手続きと機能はめったにない、疑わしい新しいものです。 治療を呼び出すことは高価な喜びでした。 メインロジックから分離されたコードの一部は困惑していました!
悲観的な時代があった。
しかし、その後、OOPの光線が世界中に輝きました...確かに、数十年間1誰もこれに気づきませんでした。 GUI 2が登場するまでは、結局のところ、OOPが非常に不足していました。 ウィンドウ内のボタンをクリックすると、ボタン(またはその代表)にメッセージ「Pressing」 3を送信して結果を取得するよりも簡単なことは何ですか?
そして、OOPが始まりました。 多くの4冊の本が書かれており、数え切れないほどの5つの記事が飼育されています。 それで今日、誰でもオブジェクト指向プログラミングを行うことができますよね?
悲しいかな、コード(およびインターネット)はそれが間違っていると言っています
最も熱烈な議論と最大の誤解は、しばしば「 継承よりも合成を好む 」というマントラによって表される、構成と継承の間の選択を引き起こすようです。 これについてお話します。
マントラが害を及ぼすとき
私はマントラのファンではありませんが、日常生活では、「継承よりも構成を優先する」ことは一般に普通です 。 彼らはしばしば真実の粒を持っているという事実にもかかわらず、誘惑に屈し、スローガンに無頓着に追いかけることはあまりにも簡単であり、その背後に隠されているものを理解していません。 そして、それは常に横に出てきます。
「Inheritance is Evil」 6のような見出しのある黄undの記事も、特に著者が最初に継承を誤って適用し、それがすべてのせいだと結論づけることによって下書きを正当化しようとする場合、私には向いていません。 まあ、「ハンマーを吸うのは、彼らがねじをねじ込むことができないからです」のように。
基本から始めましょう。
定義
記事の後半で、OOPは、プロパティ、メソッド、および単純な(単一の)継承を持つクラスをサポートする「クラシック」オブジェクト言語として理解します。 インターフェイス、不純物、アスペクト、多重継承、デリゲート、クロージャ、ラムダはありません-最もシンプルなものだけです:
- クラス:フィールドとメソッドのセットとして定義される、おそらく祖先(スーパークラス)を持つドメインからの名前付きエンティティ。
- フィールド:特定のタイプの名前付きプロパティ。特に、別のオブジェクトを参照する場合があります(構成を参照)。
- メソッド:クラスの動作を実装する名前付き関数またはプロシージャ(パラメーターありまたはパラメーターなし)。
- 継承:クラスは継承することができます-デフォルトで使用-その祖先のフィールドとメソッド。 継承は推移的です。クラスは、3番目から継承する別のクラスから継承でき、基本クラス(通常は
Object
)まで継承できます。おそらく暗黙的です。 相続人は、デフォルトの動作を変更するためにいくつかのメソッドとフィールドをオーバーライドできます。 - 構成:フィールドのタイプがClassの場合、このクラスの別のオブジェクトへのリンクを含めることができるため、2つのオブジェクト間の接続が作成されます。 単純な関連付け、集約、構成の違いのジャングルに入ることなく、指で判断しましょう。構成とは、あるオブジェクトが他の機能を部分的または完全に提供することです。
- カプセル化:オブジェクトを個別のフィールドやメソッドのセットとしてではなく、単一のエンティティとして扱い、クラスの実装を隠して保護します。 クライアントコードがパブリックインターフェイスしか知らない場合、実装の詳細に依存することはできません。
継承は基本です
継承は、OOPの基本概念です。 プログラミング言語にはオブジェクトとメッセージがありますが、継承がなければオブジェクト指向ではありません(オブジェクトのみに基づいていますが、ポリモーフィックのままです)。
...構成のように
構成は、あらゆる言語の基本的な特性でもあります。 言語が構成をサポートしていない場合でも(これは最近では珍しいことですが)、人々は依然として部品やコンポーネントの観点から考えるでしょう。 構成がなければ、部品の複雑な問題を解決することは不可能です。
(カプセル化も基本的なことですが、今では彼女についてではありません)
それで、大騒ぎは何からですか?
さて、構成と継承の両方が基本ですが、問題は何ですか?
しかし、実際には、一方が他方を常に置き換えることができる、または最初の方が2番目よりも良いまたは悪いと考えるかもしれません。 ソフトウェア開発は常に合理的なバランス、妥協の選択です。
構成は多かれ少なかれ単純で、私たちは人生で絶えず遭遇しています。椅子には脚があり、壁にはレンガやセメントなどがあります。 しかし、継承は、その単純な定義にもかかわらず、適用方法について慎重に考えないと、すべてを複雑にし、混乱させる可能性があります。 継承は非常に抽象的なものであり、それについて話すことはできますが、触れないでください。 もちろん、コンポジションを使用して継承を模倣することもできますが、これは通常は大騒ぎです。 合成の目的は何ですか?明らかなことは、部品から全体を組み立てることです。 しかし、継承はより困難です。なぜなら、それは一度に約2つのことだからです:意味とメカニズムについて。
意味的継承
生物学のように、分類群の分類はそれらを階層に編成するため、継承は主題領域からの概念の階層を反映します。 一般的なものから特定のものまでそれらを整理し、関連するアイデアを階層ツリーのブランチに収集します。 クラスの意味(セマンティクス)は、ほとんどの部分がインターフェースで表現されます-クラスが理解できるメッセージのセットですが、クラスが応答するメッセージによっても決定されます。 祖先から継承-祖先が理解できるすべてのメッセージを理解するだけでなく、祖先の振る舞いを保持するために(約Per。)答えることもできるように親切にしてください。コンポーネントとしての祖先インスタンス。 クラスが完全にシンプルで、ほとんどロジックを持たず、その名前に大きな意味的負荷がかかっている場合でも、開発者はサブジェクト領域に関する重要な結論を導き出すことに注意してください。
機械的継承
継承について機械的に言えば、継承は基本クラスのデータ(フィールド)と動作(メソッド)を受け取り、それらを再利用したり、継承者に追加したりできることを意味します。 メカニズムの観点から、子孫が祖先の実装(コード)を継承する場合、そのインターフェイスは必然的に受信されます。
ほとんどのオブジェクト指向言語における継承7のこの二重の性質は、誤解のせいだと確信しています。 多くの人々は、継承はコードを再利用することだと信じていますが、それはそれだけではありません。 再利用のために過剰使用が与えられた場合、アーキテクチャの災害を待ちます。 以下に例をいくつか示します。
継承しない方法。 例1
class Stack extends ArrayList { public void push(Object value) { … } public Object pop() { … } }
クラスStack
ように見えますが、すべて順調です。 しかし、そのインターフェースを注意深く見てください。 Stackと呼ばれるクラスには何が必要ですか? push()
およびpop()
メソッド、その他。 そして私たちと? get()
、 set()
、 add()
、 remove()
、 clear()
、およびArrayList
から継承されたArrayList
のジャンクがあります。これらはスタックにはまったく必要ありません。
望ましくないすべてのメソッドを再定義することは可能です。また、一部(たとえばclear()
)はニーズに合わせて調整することもできますが、1つの設計エラーのために少し手間がかかりませんか? 実際、3つ:1つのセマンティック、1つの機械的および1つの組み合わせ:
- ステートメント「Stack is ArrayList」は偽です。
Stack
ArrayList
サブタイプでStack
ません 。 スタックのタスクは、LIFOルール(last come、first go)の実装を確実にすることです。これは、プッシュ/ポップインターフェイスでは簡単に満たされますが、ArrayList
インターフェイスでは尊重されません。 -
ArrayList
から機械的に継承すると、カプセル化がArrayList
ます。 クライアント側のコードは、ArrayList
を使用してスタック要素を格納することを決定したことを認識しないでください。 - 最後に、
ArrayList
を介してスタックを実装するには、2つの異なるサブジェクト領域を混在させますArrayList
はランダムアクセスのコレクションであり、スタックは厳密に制限された(任意ではない)アクセスを持つキューの世界の概念です
最後の点は一見重要ではありませんが、重要なことです。 それを詳しく見てみましょう。
継承しない方法。 例2
継承でよくある間違いは、ドメインからモデルを作成し、完成した実装からモデルを継承することです。 したがって、特定のサブセットで顧客の一部( Customer
クラス)を選択する必要があるとしましょう。 簡単! ArrayList<Customer>
を継承し、 CustomerGroup
と呼んでラッシュします。
そこにあった。 そうすることで、2つの主題領域を再び混同します。 これを避けるようにしてください:
-
ArrayList<Customer>
はすでにリストの継承者であり、コレクション型ユーティリティであり、既成の実装です。 -
CustomerGroup
は完全に異なるものです-サブジェクトエリア(ドメイン)からのクラス。 - サブジェクト領域のクラスは実装を使用する必要があり、それらを継承しないでください。
ドメイン層は、内部のすべてがそこで行われる方法を知らないはずです。 私たちのプログラムが何をしているのかについて話すとき、私たちは主題分野の概念で運営しており、内部構造のニュアンスに気を取られたくありません。 継承にコード再利用ツールのみが含まれている場合、このトラップに何度も陥ります。
単一の継承ではありません
単一継承は、依然として最も人気のあるOOPモデルです。 必然的に実装の継承が必要になり、クラス間の強力なリンク(カップリング-約Per。)につながります。 問題は、セマンティックとメカニカルの両方の両方のニーズに対応する継承のブランチが1つしかないことです。 一方に使用すると、もう一方には使用できなくなります。 もしそうなら、多重継承はすべてを修正できますか?
いや 継承関係は、インストルメンタル(データ構造、アルゴリズム、ネットワーク)と適用(ビジネスロジック)のサブジェクト領域間の境界を越えてはなりません。 CustomerGroupがArrayList<Customer>
を継承すると同時に、たとえばDemographicSegmentの場合、2つのサブジェクト領域が絡み合い、オブジェクトの「種の所属」は明らかになりません。
(少なくとも私の観点からは)そうすることが望ましいです。 言語で使用可能なインストルメンタルクラスを最小限に継承します。これは、ロジックの「機械的」部分を実装するのに十分です。 次に、結果のパーツをコンポジションで接続しますが、継承では接続しません。 言い換えれば:
他のツールのみがツールから継承できます。
これは初心者にとって非常によくある間違いです。 それは驚くことではありません。なぜなら、それはとても簡単に継承できるからです。 これが間違っている理由についての議論に出くわすことはまれです。 繰り返しますが、ビジネスエンティティはツールではなく、ツールを使用する必要があります。 ハエ(ツール)-個別、カツレツ(ビジネスモデル)-個別。
では、いつ継承が必要ですか?
継承する
ほとんどの場合-そして同時に最大のリターン-継承は、互いにわずかに異なるオブジェクトを記述するために使用されます(元の用語は「差分プログラミング」という用語を使用します-およそ)。たとえば、小さな追加の特別なボタンが必要です。 通常、既存のButtonクラスから継承します。 新しいクラスはまだボタンなので、ButtonクラスのAPI、その動作、実装を完全に継承します。 新しい機能は既存のものにのみ追加されます。 ただし、機能の一部が相続人から削除された場合、これは継承が必要かどうかを考える機会です。
継承は、類似のエンティティと概念をグループ化し、クラスのファミリを定義する場合、および一般的にサブジェクト領域を説明する用語と概念を整理する場合に最も役立ちます。 多くの場合、サブジェクトロジックの大部分が既に実装されていると、最初に選択された継承階層が機能しなくなります。 すべて順調に進んだ場合、これらの階層を分解して再スタックすることを恐れないでください9 。
構成または継承:何を選択しますか?
両方が適切と思われる状況では、2つの平面で設計を見てください。
- ビジネスオブジェクトの構造と機械的な実行。
- 意味とそれらがどのように相互作用するかによって、それらは何を意味します。
継承が同じプレーン内にある限り、すべてが正常です。 しかし、階層が一度に2つのプレーンを通過する場合、これは悪い症状です。
たとえば、あるオブジェクトが別のオブジェクトの中にあるとします。 内部オブジェクトは、外部の動作の重要な部分を実装します。 外部オブジェクトには、内部オブジェクトにパラメータを愚かに転送し、その結果を返すプロキシメソッドがたくさんあります。 この場合、少なくとも部分的に内部オブジェクトから継承する価値があるかどうかを確認してください。
もちろん、肩の頭に代わる指示はありません。 オブジェクトモデルを構築するとき、一般的に考えることが有用です。 ただし、特定のルールが必要な場合は、お願いします。
次の場合に継承します。
- 両方のクラスは同じサブジェクト領域からのものです。
- 相続人は( LSPに関して)祖先の正しいサブタイプです。
- 祖先コードが必要であるか、相続人に適しています
- 相続人は基本的にロジックを追加します
これらの条件はすべて同時に満たされる場合があります。
- サブジェクト領域から高レベルのロジックをモデリングする場合
- ライブラリとそれらの拡張機能を開発するとき
- 差分プログラミングの場合(著者は再び「差分プログラミング」という用語を使用します。明らかに、 DDP以外の何かを理解する-およそトランス。)
これが当てはまらない場合、ほとんどの場合、継承は必要ありません。 しかし、構成を継承よりも「優先」する必要があるからではなく、「より良い」からでもありません。 特定のアプリケーションに最適なものを選択してください。
これらのルールが、2つのアプローチの違いを理解するのに役立つことを願っています。
素敵なコーディングをしてください!
あとがき
ThoughtWorksの貴重な貢献とコメントに感謝します: Pete Hogson 、Tim Brown、Scott Robinson、 Martin Fowler 、Mindy Ohr、Sean Newham、 Sam Gibson 、Mahendra Kariya。
1
最初の公式オブジェクト指向言語であるSIMULA 67は、1967年に登場しました。
2
システムおよびアプリケーションプログラマは、1980年代半ばにC ++を採用しましたが、OOPが一般に受け入れられるまでにさらに10年が経過しました。
3
pub / sub、デリゲートなどについては説明せず、意図的に単純化して、記事が膨らまないようにします。
4
この記事の執筆時点で、AmazonはOOPに関する24,777本を提供しています。
5
「オブジェクト指向プログラミング」というフレーズをGoogleで検索すると、800万件の結果が得られます。
6
Google検索では、「継承は悪」という結果として37,600の結果が返されます。
7
言語の複雑さにより、感覚(インターフェース)と機構(実行)を分離できます。 D言語仕様の例を参照してください。
8
悲しいことに、Javaでは、 Stack
Vector
から継承されます。
9
継承による再利用のための設計は、この記事の範囲外です。 設計は、基本クラスを使用する人と相続人を必要とする人の両方のニーズを満たす必要があることに留意してください。
翻訳者はTelegramのOOPチャットに感謝します。