10

我遇到了以下几点the advantage of object composition over class inheritance。但是我经常在很多文章中看到下面这句话

在对象组合中,功能是在运行时通过对象收集对其他对象的引用动态获取的。这种方法的优点是可以在运行时替换实现。这是可能的,因为对象只能通过它们的接口来访问,所以只要它们具有相同的类型,就可以用另一个对象替换一个对象。

但怀疑可能是幼稚的,因为我是初学者。如何在运行时替换实现?如果我们写一行新代码,难道我们不需要编译来反映变化吗?那么是什么意思replacing at runtime呢?相当混乱。或任何其他魔术,幕后活动发生。任何人都可以请回复。

4

6 回答 6

4

想一想Stack. 一个简单的实现在幕后Stack使用。List如此天真,您可以扩展ArrayList. 但是现在如果你想要一个Stack由 a 支持的单独的LinkedList,你将必须有两个类:ArrayListStackLinkedListStack. (这种方法也有在 a 上暴露List方法的缺点Stack,这违反了封装)。

如果您改用组合,则List支持 的Stack可以由调用者提供,并且您可以拥有一个Stack可以采用 aLinkedList或 a 的类ArrayList,具体取决于用户所需的运行时特性。

简而言之,实现“在运行时改变”的能力不是指类的实例能够在运行时改变它的实现,而是类在编译时不知道它的精确实现是什么。

另请注意,使用组合的类不需要允许在运行时(由调用者)选择委托实现。有时这样做会违反封装,因为它会给调用者更多关于类内部的信息,而不是期望的。在这些情况下,组合仍然具有仅公开抽象方法的好处,并允许在以后的修订中更改具体实现。

现实生活中的例子

顺便说一句,我使用 Stack 的例子,因为它不是纯粹的假设。 Java 的 Stack 类实际上是扩展Vector的,这使它永远承载着同步的包袱和数组支持列表的性能特征。因此,强烈建议不要使用该类。

在 Java 库中也可以找到正确使用集合组合的完美示例Collections.newSetFromMap(Map)。由于 anyMap可用于表示 a Set(通过使用虚拟值),因此此方法返回Set 传入的a 组成Map。返回的Set然后继承Map它包装的特性,例如:可变性、线程安全和运行时性能——所有这些都无需为、、等创建并行Set实现。ConcurrentHashMapImmutableMapTreeMap

于 2013-05-04T05:39:39.603 回答
3

有两个强有力的理由更喜欢组合而不是继承:

  • 避免类层次结构中的组合爆炸。
  • 可以在运行时修改

假设您正在为比萨店编写一个订购系统。你几乎肯定会有一个比萨课......

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!
}

简而言之,使用继承来处理沿多个轴的多样性会导致类层次结构中的组合爆炸。

第二个问题(运行时修改)也源于此。假设客户查看了他们的 LargeThinCrustMushroomPizza 的价格,呆住了,并决定他们宁愿选择 MediumThinCrustMushroomPizza?现在你被困在制作一个全新的对象只是为了改变那个属性!

这就是组合的用武之地。我们观察到“pepperoni Pizza”确实与 Pizza 具有“is-a”关系,但它也满足与 Pepperoni 的“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 ,客户可以选择更小的尺寸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
    }
}

所有的打印机类现在都可以用于不同的目的。TestPrinter当我们运行测试时,可以用作模拟或存根。FilePrinter并且NetworkPrinter每个在打印时处理特定情况。所以假设我们有一个 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 回答
0

回答这个很有趣。我不确定您是否使用过工厂模式。但是,如果您通过该示例理解这一点应该很好。让我试着把它放在这里:假设你有一个父类叫做 Pet 定义在这里 package com.javapapers.sample.designpattern.factorymethod;

//super class that serves as type to be instantiated for factory method pattern
public interface Pet {

 public String speak();

}

并且有几个子类,例如 Dog、Duck 等,示例如下:

package com.javapapers.sample.designpattern.factorymethod;

//sub class 1 that might get instantiated by a factory method pattern
public class Dog implements Pet {

 public String speak() {
 return "Bark bark...";
 }
}

package com.javapapers.sample.designpattern.factorymethod;

//sub class 2 that might get instantiated by a factory method pattern
public class Duck implements Pet {
 public String speak() {
 return "Quack quack...";
 }
}

还有一个工厂类,它根据输入类型为您返回一个 Pet,示例如下:

package com.javapapers.sample.designpattern.factorymethod;

//Factory method pattern implementation that instantiates objects based on logic
public class PetFactory {

 public Pet getPet(String petType) {
 Pet pet = null;

 // based on logic factory instantiates an object
 if ("bark".equals(petType))
 pet = new Dog();
 else if ("quack".equals(petType))
 pet = new Duck();
 return pet;
 }
}

现在让我们看看在运行时我们如何根据输入创建不同类型的宠物,示例在这里

//using the factory method pattern
public class SampleFactoryMethod {

    public static void main(String args[]) {

        // creating the factory
        PetFactory petFactory = new PetFactory();

        System.out.println("Enter a pets language to get the desired pet");
        String input = "";

        try {
            BufferedReader bufferRead = new BufferedReader(
                    new InputStreamReader(System.in));
            input = bufferRead.readLine();



            // factory instantiates an object
            Pet pet = petFactory.getPet(input);

            // you don't know which object factory created
            System.out.println(pet.speak());

        } catch (IOException e) {
            e.printStackTrace();
        }


    }

}

现在,如果您针对不同类型的输入(例如“bark”或“quack”)运行程序,您将获得不同的宠物。您可以更改上述程序以获取不同的输入并创建不同的宠物。

在这里,它回答了您的问题,即在不更改代码的情况下,仅根据输入类型,您会获得不同的行为宠物。

希望能帮助到你!

于 2013-05-04T06:02:05.320 回答
0

如何在运行时替换实现?

让我们使用一些代码示例来照亮这一天(一个每次读取新行的循环,并重新打印到目前为止读取的所有行):

List<String> myList = new ArrayList<String>(); // chose first "implementation"

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
    String line = br.readLine(); // do something like read input from the user
    myCounter.resetChronometer(); // hypothetical time counter

    myList.add(line); // add the line (make use my "implementation")
    // and then do some final work, like printing...
    for(String s: myList) {
        System.out.println(s); // print it all...
    }

    //But, hey, I'm keeping track of the time:
    myCounter.stopChronometer();
    if (myCounter.isTimeTakenTooLong())
        // this "implementation" is too slow! I want to replace it.
        // I WILL replace it at runtime (no recompile, not even stopping)
        List<String> swapList = myList; // just to keep track...

        myList = new LinkedList<String>(); // REPLACED implementation! (!!!) <---

        myList.addAll(swapList); // so I don't lose what I did up until now

        // from now on, the loop will operate with the 
        // new implementation of the List<String>
        // was using the ArrayList implementation. Now will use LinkedList
    }
}

正如您所说:这是 [only] 可能的,因为对象 [ myList] 只能通过它们的接口 ( List<String>)访问。(如果我们声明myListArrayList<String> myList,这将永远不可能......)

于 2013-05-04T06:07:32.803 回答
0

那就是多态性,它是OOP的核心概念。

它的意思是“具有多种形状的状态”或“具有不同形式的能力”。当应用于 Java 等面向对象的编程语言时,它描述了一种语言通过单一、统一的接口处理各种类型和类的对象的能力。

正如马克所说, List 是一个统一接口,它的不同实现就像ArrayList.....等

于 2013-05-04T05:40:42.873 回答