如何在运行时替换实现以进行对象组合(接口继承)

时间:2013-05-04 05:35:23

标签: java

我遇到了以下观点the advantage of object composition over class inheritance。但我经常在许多文章中看到以下句子

  
    

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

  

但怀疑可能是天真的,因为我是初学者。如何在运行时替换实现?如果我们编写一行新代码,是否需要编译以反映更改?那意味着什么是replacing at runtime?很混乱。 或任何其他魔法,幕后活动发生。任何人都可以回复。

6 个答案:

答案 0 :(得分:4)

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

如果您使用的是组合,则调用者可以提供List来支持Stack,您可以拥有一个Stack类,可以使用LinkedList }或ArrayList,具体取决于用户所需的运行时特性。

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

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

现实生活中的例子

顺便说一下,我使用Stack的例子,因为它不是纯粹的假设。 Java's Stack class实际上扩展了Vector,这使得它永远带有同步的包袱和阵列支持列表的性能特征。因此,非常不鼓励使用该课程。

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

答案 1 :(得分: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呢?现在你只是为了改变那个属性而制造一个全新的对象!

这就是作曲的来源。我们观察到“意大利辣香肠比萨饼”确实与比萨有“is-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())将做出适当的响应 - 也就是说,运行时的行为方法可能会根据对象的运行时组成而有所不同。

这并不是说继承很糟糕。但是,如果可以使用构图而不是继承来表达多种物体(如比萨饼),通常应首选构图。

答案 2 :(得分: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可以用作模拟或存根。 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

答案 3 :(得分:0)

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

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

正如标记所说List是一个Uniform接口,它的不同实现就像ArrayList ..... etc

答案 4 :(得分:0)

这很有意思回答。我不确定你是否使用过工厂模式。但是,如果你有理解这个例子应该是好的。让我试着把它放在这里: 假设您在此处定义了一个名为Pet的父类 包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;
 }
}

现在让我们看看在运行时如何根据输入创建不同类型的Pets,此处示例

//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”,您将得到一个不同的宠物。您可以更改上述程序以获取不同的输入并创建不同的宠物。

这里它回答了你的问题,即在不改变代码的情况下,只需根据输入类型得到不同的行为宠物。

希望它有所帮助!

答案 5 :(得分: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
    }
}

就像你说的那样: 这是唯一可能的,因为对象[ myList ]只能通过其接口访问( List<String> 即可。 (如果我们将myList声明为ArrayList<String> myList,这将永远不可能......)