web-dev-qa-db-ja.com

子クラスが親インターフェースとそのクラスプロパティの両方を必要とする場合、継承よりも構成を優先する必要がありますか?

継承よりも構成を優先する必要があるのはなぜですか? によると、継承よりも構成を優先する必要があります。しかし、インターフェイスとクラスメンバーに一般的な方法でアクセスする必要がある場合はどうなりますか?たとえば、親クラスShapeといくつかの子クラスがあります。

public class Shape{
    private String customId;
    public String getCustomId(){
        return customId;
    }
    public void setCustomId(String aCustomId){
        customId=aCustomId;
    }

    public abstract void draw();
}

public class Circle extends Shape{
    @Override
    public void draw(){
    }
} 

Shapeインターフェースを使用するためのいくつかのコード

for(Shape s : shapeArray){
    s.draw();
    //access customId
    if(s.getCustomId().equals(...)){
    }
}

...

public class User{
    private Shape shape;
    public void printInfo(){
        System.out.println("User Shape id:"+shape.getCustomId());
    }
}

しかし、それは継承よりも構成に違反しているので、継承を構成+インターフェースに変更します。

public interface Shape{
    String getCustomId();
    void setCustomId(String aCustomId);
    void draw();
}

public class Circle implements Shape{
    private String customId;
    public String getCustomId(){
        return customId;
    }
    public void setCustomId(String aCustomId){
        customId=aCustomId;
    }

    public void draw(){
    }
}

この場合、次の理由により、構成がより保守しづらいことがわかりました。

  1. 新しいクラスを追加するときは、コピーして貼り付ける必要があります

    private String customId;
    public String getCustomId(){
        return customId;
    }
    public void setCustomId(String aCustomId){
        customId=aCustomId;
    }
    
  2. Shapeに新しい子クラスメンバーを追加する必要がある場合は、すべての子クラスを編集して、クラスメンバー、ゲッター、セッターを追加する必要があります。

だから私の質問は、クラスメンバーとメソッドの両方に一般的な方法でアクセスする必要がある場合、「継承よりも構成」が必要ですか?

2
ocomfd

申し訳ありませんが、ここには構成がありません。私はインターフェース崇拝を見ているだけです。

継承よりも構成を優先するには、インターフェースを盲目的に平手打ちするだけではありません。作成して委任する必要があります。

public interface Id{
    String getCustomId();
    void setCustomId(String aCustomId);
}

public class IdPlain implements Id { 
    public String getCustomId(){ return customId; }
    public void setCustomId(String aCustomId){ customId=aCustomId; }
    private String customId;
}

public class Circle {
    Circle(Id id){ this.id = id; } // Circle is composed of Id
    public String getCustomId(){ return id.getCustomId(); } // and deligates to Id
    public void setCustomId(String aCustomId){ id.setCustomId(aCustomId); }
    private Id id;

    public void draw(){ // yet Circle can still do it's own thing.
    }
} 

new Circle( new IdPlain() ).draw();

継承を避けるためだけに多くの作業が必要なようですね。しかし、これについて考えてください:

new Circle( new IdFancy() ).draw();

このようなキーボードのタイピングを犠牲にして、ある程度の柔軟性は必要ない場合は、問題ありません。継承に固執する。


私はCircleのインターフェースを公開していません。これは、Circleがどのように使用されるのかまだわからないためです。 Circleを使用するクライアントは、インターフェースを所有するクライアントです。 Circleのインターフェースを、それがどのように使用されるかを知らずに公開すると、時期尚早のインターフェース設計になります。tm

ここでは、ロールインターフェイスについて議論しています。そのための優れた原則は、インターフェースの分離です。つまり、Circleが実際にどのように使用されるかがわかるまで、Circleが実装するインターフェースを設計しません。そうでなければ、私はインターフェースの投機的な爆発を取得しますtmIdDrawableICircleReadOnlyIdWriteOnlyId。いえ、いいえ。本当に必要なものがすべてわかったら、それを設計します。

Circleクラスを今すぐ書き終えて、それがどのように使用されるかがわかったときに、後でCircleを変更しないでください。残念ですが、それがどのように使用されるかを理解するまで、インターフェイスを設計するための引数としてそれを使用することはできません。どうして?まあ、継承のために:

public CircleDrawer inherits Circle implements Drawable {}

(私はここで手を振っていません。それは機能します。現状のままです。1行です。)

ごめんなさい。脳死したインターフェースの言い訳は本当にありません。

12
candied_orange

おそらく、あなたのケースでの最良のアプローチは、2つの別々のインターフェースを作成することです:

public interface IdAware<T> {
    T getId();
    void setId(T id);
}

public interface Shape {
    void draw();
}

そして結合されたインターフェース:

public interface ShapeIdAware<T> extends Shape extends IdAware<T> {}

特定の形状は、Id、Shape、ShapeIdAwareインターフェースを実装できます。

public class Circle implements ShapeIdAware<String> {
    private String id;
    public String getId(){
        return id;
    }
    public void setId(String id){
        this.id=id;
    }

    public void draw(){
    }
}

public class Rectangle implements Shape {

    public void draw(){
    }
}

Idにジェネリック型を使用すると、IdにString、Integer、またはその他の型を柔軟に使用できます。

IdAwareインターフェースの抽象的な基本実装を提供できます。

public abstract class AbstractIdAware<T> implements IdAware<T> {
     private T id;
     public T getId() {
           return id;
     }
     public setId(T id) {
        this.id = id;
     }
}

すると、Circleクラスは次のようになります。

public class Circle extends AbstractIdAware<String> implements Shape {    
    public void draw() {
    }
}
1
Pavel Molchanov

構成は柔軟で、実装の継承は短いです。選択はコンテキストに依存します。

基本クラスが内部クラスであり、決して変更されない場合、継承は問題ありません。

残念ながら、そのようなクラスは狭い範囲で使用される小さなユーティリティであることが多いため、コードの再利用のメリットはごくわずかです。

0
Basilevs

私は @ candied_orange に同意します。「構成」ソリューションには構成がありません。

あなたの例では、2つの直交する責任があります。描画の責任は、形状の種類(円、四角など)によって異なります。識別可能であるという別の責任があります。これは形状の種類によって異なりませんが、独自の理由で変化します。理想的には、可能な限り 個別の責任 を目指します。責任の要件の変更は他の責任であり、それらは別個のコードであるため、これにより保守性が向上します。これにより composability も改善されます。これは、手元にあるタスクを実行するために、さまざまな方法で小さな断片をたくさん集めることができるためです。

追跡する必要があるもう1つのことは、依存関係がどのように指し示されるかです。 安定性の低いものはより安定したものに依存する を目指すべきです。これにより、コードに関係のない変更から保護され、保守性が向上します。

これを念頭に置いて、形状の階層を見ることができます。描画コードは形状によって変化する(安定性が低い)ことがわかりますが、その複雑さは単純な(小さなi)インターフェイスであるdrawメソッドの背後にうまく隠されています。このインターフェイスは、サービスプロバイダー(シェイプの実装)と呼び出し元の両方に対して非常に安定しています。すべての形状(この場合はカスタムID)に適用される属性もあります。ご指摘のとおり、この要件は随時変更される可能性があるため、あまり安定していません。形状の実装よりも安定しておらず、drawインターフェイスよりも安定性が低いようです。

最初に、drawコンセプトのコードインターフェイス(big I)を作成しましょう。

public interface Drawable {
    void draw();
}

これで、Drawableを実装できます。

public class Circle implements Drawable {
    private Point center;
    private int radius;

    public Circle(Point center, int radius) {
        this.center = center;
        this.radius = radius;
    }

    @Override
    public void draw() { /* Draw stuff */ }
}

// etc with more drawable shapes

シェイプクラスにカスタムIDのメンバーまたはメソッドを追加しなかったことがわかります。 IDの責任は、Drawableであることの責任と同じであり、Drawableであることとは別です。代わりに、IDの責任をencapsulateするための別の場所が必要であり、composabilityの場所を提供します。

public class Displayable {
    private Drawable shape;
    private String customId;

    public Displayable(Drawable shape, String customId) {
        this.shape = shape;
        this.customId = customId;
    }

    public String getCustomId() {
        return this.customId;
    }

    public void draw() {
        this.shape.draw();
    }
}

アイデンティティーの責任を含む新しいクラスを作成しました。これは、コンストラクタを介してDrawable構成され、その構成されたDrawableにすべての描画タスクを委任します。また、識別可能な形状インスタンスのカスタムIDにアクセスするためのメソッドもあります。

これで、識別可能性のためにより多くのプロパティが必要になった場合、それらをDisplayableクラスに追加できます。これらのプロパティは1か所で利用できますが、すべての異なる形状で構成できます。さらに多くの形状が必要な場合は、Displayableに簡単に構成できます。

0
cbojar