継承を修正しますか?

最初に、私がどのようにして素晴らしいアイデアを思いついたのかについての長い紹介がありました(冗談です、これらはTS / JSのミックスインC ++のポリシーです)、それについての記事です。私はあなたの時間を無駄にしません、これが今日のお祝いの犯人です(慎重に、JSの5行):



function Extends(clazz) {
    return class extends clazz {
        // ...
    }
}


それがどのように機能するかを説明させてください。通常の継承の代わりに、上記のメカニズムを使用します。次に、オブジェクトを作成するときにのみ基本クラスを指定します。



const Class = Extends(Base)
const object = new Class(...args)


これが私の母の友人のクラス継承の息子であり、継承を真のOOPツールのタイトルに戻す方法であることを納得させようと思います(もちろん、プロトタイプの継承の直後)。



ほとんどオフトピックではありません
, , , pet project , pet project'. , .





名前に同意しましょう:これはまだ少し異なる意味ですが、私はこのテクニックをミックスインと呼びますこれらがTS / JSのミックスインであると言われる前は、LBC(後期バインドクラス)という名前を使用していました。







クラス継承の「問題」



私たちは皆、「誰もが」クラスの継承を「嫌う」方法を知っています。彼の問題は何ですか?それを理解し、同時にミックスインがそれらをどのように解決するかを理解しましょう。



実装の継承によりカプセル化が解除されます



OOPの主なタスクは、データとその操作をバインドすることです(カプセル化)。あるクラスが別のクラスから継承すると、この関係は壊れます。データはある場所(親)にあり、操作は別の場所(継承者)にあります。さらに、継承者はクラスのパブリックインターフェイスをオーバーロードできるため、基本クラスのコードも継承されたクラスのコードも、オブジェクトの状態がどうなるかを個別に知ることはできません。つまり、クラスは結合されます。



ミックスインは、次に、結合を大幅に減らします。継承クラスを宣言するときに基本クラスがない場合、継承者がどの基本クラスに依存する必要があるか。しかし、これとメソッドのオーバーロードが遅れたおかげで、「Yo-yo問題」残っています。デザインで継承を使用する場合、それから逃れることはできませんが、たとえば、Kotlinキーワードopenoverrideは、状況を大幅に緩和するはずです(私にはわかりませんが、Kotlinにあまり詳しくありません)。



不要なメソッドの継承



リストとスタックを使用した典型的な例:リストからスタックを継承すると、リストインターフェイスのメソッドがスタックインターフェイスに入り、スタック不変条件に違反する可能性があります。これが継承の問題であるとは言えません。たとえば、C ++にはこれにプライベートな継承があるため(そして、を使用して個々のメソッドを公開できますusing)、これはむしろ個々の言語の問題です。



柔軟性の欠如



  1. , : . , , : , , cohesion . , .
  2. ( ), . , : , .
  3. . - , . , , ? - — , .. .
  4. . - , - . , , .




クラスが別のクラスの実装から継承する場合、その実装を変更すると、継承するクラスが破損する可能性があります。、この記事が問題の非常に良い例ですStackMonitorableStack



ミックスインを使用する場合、プログラマーは、自分が作成する継承クラスが特定の基本クラスだけでなく、基本クラスのインターフェイスに対応する他のクラスでも機能する必要があることを考慮する必要があります。



バナナ、ゴリラ、ジャングル



OOPは、構成可能性を約束します。さまざまな状況で、さらにはさまざまなプロジェクトで個々のクラスを再利用する機能。ただし、クラスが別のクラスから継承する場合、継承者を再利用するには、すべての依存関係、基本クラスとそのすべての依存関係、およびその基本クラスをコピーする必要があります…。それら。バナナが欲しかったので、ゴリラ、そしてジャングルを引き出しました。オブジェクトが依存関係の反転の原則を念頭に置いて作成された場合、依存関係はそれほど悪くはありません。インターフェイスをコピーするだけです。ただし、これは継承チェーンでは実行できません。



次に、ミックスインは、継承に関連してDIPを使用することを可能にします(そして義務付けます)。



Mixinsの他の設備



ミックスインの利点はそれだけではありません。それらを使って他に何ができるか見てみましょう。



継承階層の死



クラスは相互に依存しなくなりました。インターフェイスのみに依存します。それら。実装は依存関係グラフの葉になります。これにより、リファクタリングが容易になります。ドメインモデルは、その実装から独立しています。



抽象クラスの死



抽象クラスは不要になりました。リファクタリングの第一人者から借りたJavaのファクトリーメソッドパターンの例を見てみましょう



interface Button {
    void render();
    void onClick();
}

abstract class Dialog {
    void renderWindow() {
        Button okButton = createButton();
        okButton.render();
    }

    abstract Button createButton();
}


はい、もちろん、ファクトリーメソッドはビルダーとストラテジーのパターンに進化します。しかし、これはミックスインを使用して行うことができます(Javaにファーストクラスのミックスインがあることを少し想像してみましょう)。



interface Button {
    void render();
    void onClick();
}

interface ButtonFactory {
    Button createButton();
}

class Dialog extends ButtonFactory {
    void renderWindow() {
        Button okButton = createButton();
        okButton.render();
    }
}


このトリックは、ほとんどすべての抽象クラスで実行できます。それが機能しない例:



abstract class Abstract {
    void method() {
        abstractMethod();
    }

    abstract void abstractMethod();
}

class Concrete extends Abstract {
    private encapsulated = new Encapsulated();

    @Override
    void method() {
        encapsulated.method();
        super.method();
    }

    void abstractMethod() {
        encapsulated.otherMethod();
    }
}


ここでは、フィールドはencapsulatedオーバーロードmethodと実装の両方必要ですabstractMethodつまり、カプセル化を解除しConcreteないと、クラスを子Abstractと「スーパークラス」に分割することはできませんAbstractしかし、これが良いデザインの例であるかどうかはわかりません。



タイプに匹敵する柔軟性



注意深い読者は、これらがすべてSmalltalk / Rustの特性に非常に似ていることに気付くでしょう。2つの違いがあります。



  1. Mixinインスタンスには、基本クラスになかったデータを含めることができます。
  2. ミックスインは、継承元のクラスを変更しません。ミックスイン機能を使用するには、基本クラスオブジェクトではなく、ミックスインオブジェクトを明示的に作成する必要があります。


2番目の違いは、基本クラスのすべてのインスタンスに作用する特性とは対照的に、たとえば、ミックスインが局所的に作用するという事実につながります。それがどれほど便利かはプログラマーとプロジェクトによって異なりますが、私のソリューションが間違いなく優れているとは言えません。



これらの違いにより、ミックスインは通常の継承に近づくため、このことは、継承と特性の間の面白いトレードオフのように思えます。



ミックスインの短所



ああ、それがそんなに単純だったら。Mixinsには間違いなく1つの小さな問題と1つのファットマイナスがあります。



爆発するインターフェース



インターフェイスからのみ継承できる場合は、明らかに、プロジェクトにはさらに多くのインターフェイスがあります。もちろん、プロジェクトでDIPが尊重されている場合、さらにいくつかのインターフェイスは天候に影響しませんが、すべてがSOLIDに従うわけではありません。この問題は、各クラスに基づいて、すべてのパブリックメソッドを含むインターフェイスが生成され、クラス名に言及するときに、クラスがオブジェクトのファクトリとしてのものか、インターフェイスとしてのものかを区別することで解決できます。TypeScriptでも同様のことが行われますが、何らかの理由で、生成されたインターフェイスにプライベートフィールドとメソッドが記載されています。



複雑なコンストラクター



ミックスインを使用する場合、最も難しいタスクはオブジェクトを作成することです。コンストラクターが基本クラスのインターフェースに含まれているかどうかに応じて、2つのオプションを検討してください。



  1. , , . , - . , .
  2. , . :



    interface Base {
        new(values: Array<int>)
    }
    
    class Subclass extends Base {
        // ...
    }
    
    class DoesntFit {
        new(values: Array<int>, mode: Mode) {
            // ...
        }
    }
    


    DoesntFit Subclass, - . Subclass DoesntFit, Base .
  3. 実際、別のオプションがあります。引数のリストではなく、辞書をコンストラクターに渡すことです。これは{ values: Array<int>, mode: Mode }明らかにパターンに適合するため、上記の問題を解決し{ values: Array<int> }ますが、そのような辞書の名前の予測できない衝突につながります。たとえば、スーパークラスAと継承者の両方Bが同じパラメータを使用しますが、この名前はの基本クラスのインターフェイスで指定されていませんB


結論の代わりに



私はこのアイデアのいくつかの側面を見逃したと確信しています。または、これがすでにワイルドボタンアコーディオンであり、20年前にこのアイデアを使用する言語があったという事実。いずれにせよ、コメントでお待ちしております!



ソースのリスト



neethack.com/2017/04/Why-inheritance-is-bad

www.infoworld.com/article/2073649/why-extends-is-evil.html

www.yegor256.com/2016/09/13/inheritance-is- procedural.html

refactoring.guru/ru/design-patterns/factory-method/java/example

scg.unibe.ch/archive/papers/Scha03aTraits.pdf



All Articles