オタク時間-設計パターン理論7:なぜより多くの組み合わせとより少ない継承を使用するのですか?構成を使用するか継承を使用するかを決定する方法は?

オブジェクト指向のプログラミングには、非常に古典的な設計原則があります。つまり、構成は継承よりも優れており、組み合わせの使用が多く、継承の使用が少なくなります。継承が推奨されないのはなぜですか?継承に対する構成の利点は何ですか?構成を使用するか継承を使用するかを判断する方法は?本日は、これら3つの問題について、この設計原則について詳しく説明します。

言うまでもありませんが、今日の学習を正式に始めましょう!

継承が推奨されないのはなぜですか?

継承は、クラス間のis-a関係を表すために使用される、4つの主要なオブジェクト指向機能の1つであり、コードの再利用の問題を解決できます。継承には多くの機能がありますが、継承が深く複雑すぎると、コードの保守性にも影響を与える可能性があります。したがって、継承をプロジェクトで使用すべきかどうかについて、インターネット上で多くの論争があります。多くの人々は、継承は反パターンであり、控えめに使用するか、使用しないかでさえあると考えています。なぜそのような論争があるのですか?例を通して説明しましょう。

鳥についてのクラスを設計したいとします。「鳥」の抽象的な概念を抽象的なクラスAbstractBirdとして定義します。スズメ、ハト、カラスなど、さらに細分化された鳥はすべて、この抽象的なクラスを継承します。

ほとんどの鳥が飛ぶことができることを知っているので、AbstractBird抽象クラスでfly()メソッドを定義できますか?答えは否定的です。ほとんどの鳥は飛ぶことができますが、例外があります。たとえば、ダチョウは飛べません。オーストリッチはfly()メソッドで親クラスを継承し、オーストリッチは「フライング」の動作をしますが、これは明らかに現実世界の物事の理解に準拠していません。もちろん、ostrichのサブクラスのfly()メソッドをオーバーライドして、UnSupportedMethodExceptionをスローさせることができると言うかもしれません。具体的なコードの実装は次のとおりです。


public class AbstractBird {
    
    
  //...省略其他属性和方法...
  public void fly() {
    
     //... }
}

public class Ostrich extends AbstractBird {
    
     //鸵鸟
  //...省略其他属性和方法...
  public void fly() {
    
    
    throw new UnSupportedMethodException("I can't fly.'");
  }
}

このデザインのアイデアは問題を解決することができますが、それは十分にエレガントではありません。なぜなら、ダチョウに加えて、ペンギンのような飛行のない鳥がたくさんいるからです。これらの飛行のない鳥については、fly()メソッドを書き直して、例外をスローする必要があります。このような設計は、一方ではコーディングの作業負荷を増加させるだけですが、他方では、後で説明する最小知識原則(最小知識原則またはディミットの法則とも呼ばれます)に違反し、すべきでないことを明らかにします。公開されたインターフェースは外部に公開されているため、クラスの使用中に誤用される可能性が高くなります。

もう一度言うかもしれませんが、AbstractBirdクラスからさらに2つの細分化された抽象クラスを派生させます。飛んでいる鳥AbstractFlyableBirdと飛んでいない鳥AbstractUnFlyableBirdで、スズメやカラスなどの飛んでいる鳥はAbstractFlyableBirdを継承します。 、ダチョウやペンギンなどの飛行のない鳥にAbstractUnFlyableBirdクラスを継承させます。特定の継承関係を次の図に示します。

ここに写真の説明を挿入
この図から、継承関係が3層になっていることがわかります。ただし、全体として、現在の継承関係は比較的単純であり、レベルは比較的浅いため、許容できる設計アイデアと見なすことができます。さらに難易度を上げていきましょう。今のシーンでは「鳥が飛べる」だけに焦点を当てていますが、「鳥が呼べる」にも焦点を当てると、この時点でクラス間の継承関係をどのように設計するのでしょうか。

飛べますか?呼ばれますか?2つの動作を組み合わせて、4つの状況を作り出すことができます。飛ぶことができる、吠える、飛ぶことができない、吠える、飛ぶことができる、吠えることができない、飛ぶことができない、吠える。今すぐデザインのアイデアを使い続ける場合は、4つの抽象クラス(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)を定義する必要があります。

ここに写真の説明を挿入
それでも「産卵するかどうか」の振る舞いを考える必要があるとすれば、その組み合わせは爆発すると推定されます。クラスの継承レベルはますます深くなり、継承関係はますます複雑になります。そして、この種の深く複雑な継承関係は、一方では、コードの読みやすさを低下させることになります。特定のクラスがどのメソッドとプロパティを持っているかを知りたいので、親クラスのコード、親クラスの親クラスのコードを読み取る必要があります...最上位の親クラスのコードまでさかのぼります。一方、これはクラスのカプセル化特性も破壊し、親クラスの実装の詳細を子クラスに公開します。サブクラスの実装は親クラスの実装に依存し、2つは高度に結合されています。親クラスのコードが変更されると、すべてのサブクラスのロジックに影響します。

要するに、継承の最大の問題は、継承レベルが深すぎ、継承関係が複雑すぎて、コードの読みやすさと保守性に影響を与えるという事実にあります。これが、継承の使用を推奨しない理由です。では、この例の継承の問題をどのように解決できるでしょうか。最初に自分で考えてから、以下の私の説明を聞いてください。

継承に対する構成の利点は何ですか?

実際、構成、インターフェイス、および委任の3つの技術的手段を使用して、継承に存在したばかりの問題を解決できます。

先ほどインターフェースについてお話ししたとき、インターフェースには特定の動作特性があると言いました。「飛行」の行動特性については、飛行可能なインターフェースを定義し、飛んでいる鳥だけにこのインターフェースを実装させることができます。同様に、卵を呼んだり産んだりする行動特性について、TweetableインターフェースとEggLayableインターフェースを定義することができます。この設計アイデアをJavaコードに変換すると、次のようになります。


public interface Flyable {
    
    
  void fly();
}
public interface Tweetable {
    
    
  void tweet();
}
public interface EggLayable {
    
    
  void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {
    
    //鸵鸟
  //... 省略其他属性和方法...
  @Override
  public void tweet() {
    
     //... }
  @Override
  public void layEgg() {
    
     //... }
}
public class Sparrow impelents Flayable, Tweetable, EggLayable {
    
    //麻雀
  //... 省略其他属性和方法...
  @Override
  public void fly() {
    
     //... }
  @Override
  public void tweet() {
    
     //... }
  @Override
  public void layEgg() {
    
     //... }
}

ただし、インターフェイスはメソッドのみを宣言し、実装は宣言しないことがわかっています。つまり、産卵するすべての鳥はlayEgg()メソッドを再度実装する必要があり、実装ロジックは同じであるため、コードが重複します。この問題を解決する方法は?

3つのインターフェイスに対してさらに3つの実装クラスを定義できます。それらは、fly()メソッドを実装するFlyAbilityクラス、tweet()メソッドを実装するTweetAbilityクラス、およびlayEgg()メソッドを実装するEggLayAbilityクラスです。次に、組み合わせと試運転の手法を使用して、コードの重複を排除します。具体的なコードの実装は次のとおりです。


public interface Flyable {
    
    
  void fly()}
public class FlyAbility implements Flyable {
    
    
  @Override
  public void fly() {
    
     //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {
    
    //鸵鸟
  private TweetAbility tweetAbility = new TweetAbility(); //组合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
  //... 省略其他属性和方法...
  @Override
  public void tweet() {
    
    
    tweetAbility.tweet(); // 委托
  }
  @Override
  public void layEgg() {
    
    
    eggLayAbility.layEgg(); // 委托
  }
}

継承には、is-a関係の表現、多態性のサポート、コードの再利用という3つの主要な機能があることがわかっています。これらの3つの機能は、他の技術的手段によって実現できます。たとえば、is-a関係はhas-構成とインターフェイスの関係に置き換えることができます。多態性はインターフェイスを使用して実現できます。コードの再利用は構成と委任によって実現できます。したがって、理論的には、組み合わせ、インターフェース、委任の3つの技術的手段によって、継承を完全に置き換えることができます。プロジェクトでは、継承関係、特に一部の複雑な継承関係は使用または使用されません。

構成を使用するか継承を使用するかを判断する方法は?

構成の使用を増やし、継承の使用を減らすことをお勧めしますが、構成は完全ではなく、継承は役に立たないわけではありません。上記の例から、継承を構成に書き換えることは、よりきめ細かいクラス分割を行うことを意味します。これは、より多くのクラスとインターフェイスを定義する必要があることも意味します。クラスとインターフェースの増加は、多かれ少なかれコードの複雑さとメンテナンスコストを増加させます。したがって、実際のプロジェクト開発では、特定の状況に応じて、継承を使用するか組み合わせを使用するかを選択する必要があります。

クラス間の継承構造が安定していて(簡単に変更されない)、継承階層が比較的浅く(たとえば、継承関係が最大2つある)、継承関係が複雑でない場合は、継承を大胆に使用できます。逆に、システムが不安定になるほど、継承階層が深くなり、継承関係が複雑になるほど、継承ではなく構成を使用しようとします。

さらに、継承または構成を使用するいくつかの設計パターンがあります。たとえば、デコレータパターン(デコレータパターン)、ストラテジーパターン(ストラテジーパターン)、コンビネーションパターン(コンポジットパターン)などはすべてコンポジションリレーションシップを使用し、テンプレートパターン(テンプレートパターン)は継承リレーションシップを使用します。

先ほど、継承によってコードの再利用が可能になることを説明しました。継承機能を使用して、同じ属性とメソッドを抽出し、それらを親クラスで定義します。サブクラスは、親クラスのプロパティとメソッドを再利用して、コードの再利用の目的を達成します。ただし、ビジネスの観点から、クラスAとクラスBが必ずしも継承関係にあるとは限りません。たとえば、CrawlerクラスとPageAnalyzerクラスはどちらもURLスプライシング関数とセグメンテーション関数を使用しますが、継承関係(親子関係も兄弟関係もありません)はありません。コードを再利用するためだけに、親クラスは鈍く抽象化されます。これは、コードの読みやすさに影響します。背後にある設計のアイデアに精通していない同僚が、CrawlerクラスとPageAnalyzerクラスが同じ親クラスを継承しているが、親クラスがURL関連の操作のみを定義していることに気付いた場合、このコードは不可解に記述されており、理解できないと感じるでしょう。現時点では、組み合わせの使用はより合理的でより柔軟です。具体的なコードの実装は次のとおりです。


public class Url {
    
    
  //...省略属性和方法
}

public class Crawler {
    
    
  private Url url; // 组合
  public Crawler() {
    
    
    this.url = new Url();
  }
  //...
}

public class PageAnalyzer {
    
    
  private Url url; // 组合
  public PageAnalyzer() {
    
    
    this.url = new Url();
  }
  //..
}

継承を使用する必要がある特別なシナリオもいくつかあります。関数の入力パラメータタイプを変更できず、入力パラメータがインターフェイスではない場合、多態性をサポートするために、継承を使用して達成することしかできません。たとえば、FeignClientが外部クラスである次のコードでは、コードのこの部分を変更する権限はありませんが、実行時にこのクラスによって実行されるencode()関数を書き直したいと考えています。現時点では、継承を使用して達成することしかできません。


public class FeignClient {
    
     // Feign Client框架代码
  //...省略其他代码...
  public void encode(String url) {
    
     //... }
}

public void demofunction(FeignClient feignClient) {
    
    
  //...
  feignClient.encode(url);
  //...
}

public class CustomizedFeignClient extends FeignClient {
    
    
  @Override
  public void encode(String url) {
    
     //...重写encode的实现...}
}

// 调用
FeignClient client = new CustomizedFeignClient();
demofunction(client);

継承を排除し、100%組み合わせで置き換える必要があると言う人もいますが、私の意見はそれほど極端ではありません!「継承を多用し、組み合わせ、使用を減らす」というスローガンが、私たちが長い間継承を使いすぎてきたために、大声で叫ばれる理由。繰り返しますが、組み合わせは完全ではなく、継承は役に立たないわけではありません。私たちがそれらの副作用を制御し、それぞれの利点を発揮し、さまざまな状況で継承または組み合わせを使用するかどうかを適切に選択する限り、これが私たちが追求する領域です。

キーレビュー

この時点で、今日のコンテンツは終了です。マスターする必要のある知識ポイントを一緒に確認しましょう。

1.継承が推奨されないのはなぜですか?

継承は、クラス間のis-a関係を表すために使用される、4つの主要なオブジェクト指向機能の1つであり、コードの再利用の問題を解決できます。継承には多くの機能がありますが、継承が深く複雑すぎると、コードの保守性にも影響を与える可能性があります。この場合、継承がなくても、使用はできるだけ少なくする必要があります。

2.継承に対する構成の利点は何ですか?

継承には3つの主要な機能があります。それは、is-a関係を表し、多態性をサポートし、コードを再利用します。そして、これらの3つの機能は、組み合わせ、インターフェース、および試運転という3つの技術的手段によって実現できます。さらに、合成を使用すると、コードの保守性に影響を与える過度に深く複雑な継承関係の問題を解決することもできます。

3.構成と継承のどちらを使用するかを判断するにはどうすればよいですか?

構成の使用を増やし、継承の使用を減らすことをお勧めしますが、構成は完全ではなく、継承は役に立たないわけではありません。実際のプロジェクト開発では、特定の状況に応じて、継承を使用するか組み合わせを使用するかを選択する必要があります。クラス間の継承構造が安定していて、階層が比較的浅く、関係が複雑でなければ、継承を大胆に使うことができます。それどころか、私たちは継承の代わりに構成を使用しようとします。さらに、常に継承または組み合わせを使用するいくつかの設計パターンと特別なアプリケーションシナリオがあります。

おすすめ

転載: blog.csdn.net/zhujiangtaotaise/article/details/110234561