10

私は次の点に出くわしましたthe advantage of object composition over class inheritance。しかし、多くの記事で次の文をよく見かけます

オブジェクト構成では、他のオブジェクトへの参照を収集するオブジェクトによって、実行時に機能が動的に取得されます。このアプローチの利点は、実行時に実装を置き換えることができることです。これが可能になるのは、オブジェクトがそのインターフェイスを介してのみアクセスされるためです。そのため、オブジェクトが同じ型である限り、1 つのオブジェクトを別のオブジェクトに置き換えることができます。

しかし、私は初心者なので、疑いはナイーブかもしれません。実行時に実装をどのように置き換えることができますか? 新しいコード行を記述した場合、変更を反映するためにコンパイルする必要はありませんか? では とはどういう意味replacing at runtimeですか? かなり混乱します。またはその他の魔法、舞台裏での活動が発生します。どなたか回答お願いします。

4

6 に答える 6

3

継承よりも構成を優先する 2 つの強い理由があります。

  • クラス階層での組み合わせの爆発を回避します。
  • 実行時に変更可能

ピザ屋の注文システムを書いているとしましょう。あなたはほぼ確実にクラスのピザを持っているでしょう...

public class Pizza {
    public double getPrice() { return BASE_PIZZA_PRICE; }
}

そして、他のすべてが同じであれば、ピザパーラーはおそらくペパロニピザをたくさん売っています. これには継承を使用できます。PepperoniPizza はピザとの「is-a」関係を満たしているため、有効に聞こえます。

public class PepperoniPizza extends Pizza {
    public double getPrice() { return super.getPrice() + PEPPERONI_PRICE; }
}

よし、ここまではいいぞ?しかし、私たちが考慮していないことがわかるでしょう。たとえば、顧客がペパロニとマッシュルームを欲しがっていたらどうしますか? さて、PepperoniMushroomPizza クラスを追加できます。すでに問題があります。PepperoniMushroomPizza は、Pizza、PepperoniPizza、または MushroomPizza を拡張する必要がありますか?

しかし、事態はさらに悪化します。架空のピザ パーラーが、小、中、大のサイズを提供しているとします。また、クラストもさまざまです。厚い、薄い、通常のクラストがあります。継承だけを使用している場合、突然、MediumThickCrustPepperoniPizza、LargeThinCrustMushroomPizza、SmallRegularCrustPepperoniAndMushroomPizza などのクラスが作成されます...

public class LargeThinCrustMushroomPizza extends ThinCrustMushroomPizza {
    // This is not good!
}

つまり、継承を使用して複数の軸に沿った多様性を処理すると、クラス階層で組み合わせ爆発が発生します。

2 番目の問題 (実行時の変更) もこれに由来します。顧客が LargeThinCrustMushroomPizza の価格を見て、gawks を見て、代わりに MediumThinCrustMushroomPizza を購入したいと判断したとします。これで、その 1 つの属性を変更するためだけに、まったく新しいオブジェクトを作成することになりました。

これが構成の出番です。「ペパロニ ピザ」は実際にピザと「is-a」の関係を持っていますが、ペパロニとの「has-a」の関係も満たしていることがわかります。また、地殻の種類とサイズとの「has-a」関係も満たします。したがって、コンポジションを使用して Pizza を再定義します。

public class Pizza { 
    private List<Topping> toppings;
    private Crust crust;
    private Size size;

    //...omitting constructor, getters, setters for brevity...

    public double getPrice() {
        double price = size.getPrice();
        for (Topping topping : toppings) {
            price += topping.getPriceAtSize(size);
        }
        return price;
    }
}

この構成ベースのピザでは、顧客はより小さいサイズ ( pizza.setSize(new SmallSize())) を選択でき、価格 ( getPrice()) は適切に応答します。つまり、メソッドの実行時の動作は、オブジェクトの実行時の構成によって異なる場合があります。

継承が悪いと言っているわけではありません。しかし、継承の代わりにコンポジションを使用してさまざまなオブジェクト (ピザなど) を表現できる場合は、通常、コンポジションを優先する必要があります。

于 2013-05-04T16:33:33.287 に答える
1

他の回答はこれについて少し語っていますが、実行時に動作がどのように変化するかの例が役立つと思いました。インターフェースがあるとしますPrinter

interface Printer {
    void print(Printable printable);
}

class TestPrinter implements Printer {

    public void print(Printable printable) {
        // set an internal state that can be checked later in a test
    }

}

class FilePrinter implements Printer {

    public void print(Printable printable) {
        // Do stuff to print the printable to a file
    }
}

class NetworkPrinter implements Printer {

    public void print(Printable printable) {
        // Connects to a networked printer and tell it to print the printable
    }
}

すべての Printer クラスをさまざまな目的に使用できるようになりました。TestPrinterテストを実行するときに、モックまたはスタブとして使用できます。それぞれが印刷時に特定のケースを処理しますFilePrinterNetworkPrinterそこで、ユーザーがボタンを押して何かを印刷できる UI ウィジェットがあるとします。

class PrintWidget {
    // A collection of printers that keeps track of which printer the user has selected.
    // It could contain a FilePrinter, NetworkPrinter and any other object implementing the
    // Printer interface
    private Selectable<Printer> printers; 

    // A reference to a printable object, could be a document or image or something
    private Printable printable;

    public void onPrintButtonPressed() {
        Printer printer = printers.getSelectedPrinter();
        printer.print(printable);
    }

    // other methods 
}

実行時にユーザーが別のプリンターを選択して印刷ボタンを押すと、onPrintButtonPressedメソッドが呼び出され、選択されたものがPrinter使用されます。

于 2013-05-04T06:26:54.220 に答える