参考以下链接:
http://www.javaworld.com/javaworld/jw-11-1998/jw-11-techniques.html?page=2
代码重用的组合方法提供了更强大的封装 而不是继承,因为对后端类的更改不需要中断 任何只依赖于前端类的代码。例如, 更改Fruit的peel()方法的返回类型 示例不会强制更改Apple的界面,因此 不必破坏Example2的代码。
当然,如果您更改peel()
的返回类型(请参阅下面的代码),这意味着getPeelCount()
将无法再返回int
?你不是必须改变界面,否则会得到编译器错误?
class Fruit {
// Return int number of pieces of peel that
// resulted from the peeling activity.
public int peel() {
System.out.println("Peeling is appealing.");
return 1;
}
}
class Apple {
private Fruit fruit = new Fruit();
public int peel() {
return fruit.peel();
}
}
class Example2 {
public static void main(String[] args) {
Apple apple = new Apple();
int pieces = apple.peel();
}
}
答案 0 :(得分:6)
使用撰写,更改班级Fruit
并不一定要求您更改Apple
,例如,让我们更改peel
以返回{ {1}}代替:
double
现在,类class Fruit {
// Return String number of pieces of peel that
// resulted from the peeling activity.
public double peel() {
System.out.println("Peeling is appealing.");
return 1.0;
}
}
会警告精确度会丢失,但是你的Apple
类会很好,因为组合更“松散”,组合元素的变化不会打破组成类API。在我们的示例中,只需像这样更改Example2
:
Apple
如果class Apple {
private Fruit fruit = new Fruit();
public int peel() {
return (int) fruit.peel();
}
}
从Apple
(Fruit
)继承,那么您不仅会收到有关不兼容的返回类型方法的错误,而且还会还会在class Apple extends Fruit
中收到编译错误。
** 修改 **
让我们开始讨论并给出组合 vs 继承的“真实世界”示例。请注意,合成不仅限于此示例,还有更多用例可以使用该模式。
应用程序将形状绘制到画布中。应用程序不需要知道它必须绘制哪些形状,实现位于继承抽象类或接口的具体类中。但是,应用程序知道它可以创建多少不同的混凝土形状,因此添加或删除混凝土形状需要在应用程序中进行一些重构。
Example2
应用程序正在使用本机库来处理某些数据。实际的库实现可能已知,也可能未知,并且将来可能会或可能不会发生变化。这样就创建了一个公共接口,并在运行时确定了实际的实现。例如:
interface Shape {
public void draw(Graphics g);
}
class Box implement Shape {
...
public void draw(Graphics g) { ... }
}
class Ellipse implements Shape {
...
public void draw(Graphics g) { ... }
}
class ShapeCanvas extends JPanel {
private List<Shape> shapes;
...
protected void paintComponent(Graphics g) {
for (Shape s : shapes) { s.draw(g); }
}
}
因此,正如您所看到的,组合可以提供一些优于继承的优势,因为它允许代码具有更大的灵活性。它允许应用程序具有可靠的API,而底层实现在其生命周期中可能仍会发生变化。如果使用得当,组合物可以显着降低维护成本。
例如,在使用JUnit实现 Exemple 2 的测试用例时,您可能希望使用虚拟处理器并设置interface DataProcessorAdapter {
...
public Result process(Data data);
}
class DataProcessor {
private DataProcessorAdapter adapter;
public DataProcessor() {
try {
adapter = DataProcessorManager.createAdapter();
} catch (Exception e) {
throw new RuntimeException("Could not load processor adapter");
}
}
public Object process(Object data) {
return adapter.process(data);
}
}
static class DataProcessorManager {
static public DataProcessorAdapter createAdapter() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
String adapterClassName = /* load class name from resource bundle */;
Class<?> adapterClass = Class.forName(adapterClassName);
DataProcessorAdapter adapter = (DataProcessorAdapter) adapterClass.newInstance();
//...
return adapter;
}
}
以返回此类适配器,同时使用“真实”生产中的“适配器(可能依赖于操作系统)”而不更改应用程序源代码。使用继承,你很可能会破解某些东西,或者可能会编写更多的初始化测试代码。
正如您所看到的, compisition 和继承在很多方面都有所不同,并不比其他方面更受欢迎;每个都取决于手头的问题。你甚至可以混合继承和组合,例如:
DataProcessorManager
当然,这最后一个例子不干净(意思是避免它),但它显示了如何使用 组合
。底线是,static interface IShape {
public void draw(Graphics g);
}
static class Shape implements IShape {
private IShape shape;
public Shape(Class<? extends IShape> shape) throws InstantiationException, IllegalAccessException {
this.shape = (IShape) shape.newInstance();
}
public void draw(Graphics g) {
System.out.print("Drawing shape : ");
shape.draw(g);
}
}
static class Box implements IShape {
@Override
public void draw(Graphics g) {
System.out.println("Box");
}
}
static class Ellipse implements IShape {
@Override
public void draw(Graphics g) {
System.out.println("Ellipse");
}
}
static public void main(String...args) throws InstantiationException, IllegalAccessException {
IShape box = new Shape(Box.class);
IShape ellipse = new Shape(Ellipse.class);
box.draw(null);
ellipse.draw(null);
}
和DataProcessor
两个示例都是“固定”类,其API不应更改。但是,适配器类可能会更改,如果这样做,这些更改应该只影响它们的组合容器,从而将维护限制为仅限于这些类而不是整个应用程序,而不是示例1 其中任何更改整个应用程序需要进行更多更改这一切都取决于您的应用程序需要多么灵活。
答案 1 :(得分:1)
句子中的关键词是“界面”。
您几乎总是需要以某种方式更改Apple
类以容纳Fruit.peel
的新返回类型,但您无需更改其公共接口如果你使用的是组合而不是继承。
如果Apple
是 Fruit
(即继承),那么对Fruit
的公共接口的任何更改都需要更改{的公共接口{1}}也是。如果Apple
有 Apple
(即组合),那么您可以决定如何适应Fruit
类的任何更改;如果你不想,你不会被迫改变你的公共界面。
答案 2 :(得分:1)
如果您要更改Fruit.peel()
的返回类型,则还必须修改Apple.peel()
。但您不必更改Apple
的界面。
请记住:界面只是方法名称及其签名,而不是实现。
假设您要更改Fruit.peel()
以返回boolean
而不是int。然后,您仍然可以让Apple.peel()
返回int
。所以:Apple
的界面保持不变,但Fruit
已更改。
如果您将使用继承,那将无法实现:由于Fruit.peel()
现在返回布尔值,Apple.peel()
也必须返回boolean
。因此:所有使用Apple.peel()
的代码也必须更改。在合成示例中,只需要更改Apple.peel()
的代码。
答案 3 :(得分:0)
Fruit.peel()
的返回类型正从int更改为Peel
。这并不意味着Apple.peel()
的返回类型也被强制更改为Peel
。在继承的情况下,它是强制的,任何使用Apple
的客户端都必须进行更改。在组合的情况下,Apple.peel()
仍然通过调用Peel.getPeelCount()
getter返回一个整数,因此客户端不需要更改,因此Apple
的接口不会更改(或被强制使用被改变了)
答案 4 :(得分:0)
好吧,在组合案例中,Apple.peel()
的实现需要更新,但其方法签名可以保持不变。这意味着客户端代码(使用Apple
)不需要修改,重新测试和重新部署。
这与继承相反,在继承中,Fruit.peel()
方法签名的更改需要在客户端代码中进行更改。