我遇到了以下观点the advantage of object composition over class inheritance
。但我经常在许多文章中看到以下句子
在对象组合中,功能是在运行时通过收集对其他引用的对象动态获取的 对象。这种方法的优点是可以在运行时替换实现。这是可能的,因为 只能通过它们的接口访问对象,因此只要它们就可以用另一个对象替换 具有相同的类型。
但怀疑可能是天真的,因为我是初学者。如何在运行时替换实现?如果我们编写一行新代码,是否需要编译以反映更改?那意味着什么是replacing at runtime
?很混乱。
或任何其他魔法,幕后活动发生。任何人都可以回复。
答案 0 :(得分:4)
考虑Stack
的实现。 Stack
的简单实现在幕后使用List
。天真地,你可以扩展ArrayList
。但现在,如果您想要由Stack
支持单独的LinkedList
,则必须有两个类:ArrayListStack
和LinkedListStack
。 (这种方法的缺点是在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
实现}},Set
,ConcurrentHashMap
等
答案 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
可以用作模拟或存根。 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
。
答案 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
,这将永远不可能......)