Composition, I don't quite get this?

2019-09-17 16:09发布

问题:

Referring to the below link:

http://www.javaworld.com/javaworld/jw-11-1998/jw-11-techniques.html?page=2

The composition approach to code reuse provides stronger encapsulation than inheritance, because a change to a back-end class needn't break any code that relies only on the front-end class. For example, changing the return type of Fruit's peel() method from the previous example doesn't force a change in Apple's interface and therefore needn't break Example2's code.

Surely if you change the return type of peel() (see code below) this means getPeelCount() wouldn't be able to return an int any more? Wouldn't you have to change the interface, or get a compiler error otherwise?

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();
    }
}

回答1:

With a composition, changing the class Fruit doesn't necessary require you to change Apple, for example, let's change peel to return a double instead :

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

Now, the class Apple will warn about a lost of precision, but your Example2 class will be just fine, because a composition is more "loose" and a change in a composed element does not break the composing class API. In our case example, just change Apple like so :

class Apple {

    private Fruit fruit = new Fruit();

    public int peel() {
        return (int) fruit.peel();
    }
}

Whereas if Apple inherited from Fruit (class Apple extends Fruit), you would not only get an error about an incompatible return type method, but you'd also get a compilation error in Example2.

** Edit **

Lets start this over and give a "real world" example of composition vs inheritance. Note that a composition is not limited to this example and there are more use case where you can use the pattern.

Example 1 : inheritance

An application draw shapes into a canvas. The application does not need to know which shapes it has to draw and the implementation lies in the concrete class inheriting the abstract class or interface. However, the application knows what and how many different concrete shapes it can create, thus adding or removing concrete shapes requires some refactoring in the application.

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); }
   }
}

Example 2 : Composition

An application is using a native library to process some data. The actual library implementation may or may not be known, and may or may not change in the future. A public interface is thus created and the actual implementation is determined at run-time. For example :

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

So, as you can see, the composition may offer some advantage over inheritance in the sense that it allows more flexibility in the code. It allows the application to have a solid API while the underlaying implementation may still change during it's life cycle. Composition can significantly reduce the cost of maintenance if properly used.

For example, when implementing test cases with JUnit for Exemple 2, you may want to use a dummy processor and would setup the DataProcessorManager to return such adapter, while using a "real" adapter (perhaps OS dependent) in production without changing the application source code. Using inheritance, you would most likely hack something up, or perhaps write a lot more initialization test code.

As you can see, compisition and inheritance differ in many aspects and are not preferred over another; each depend on the problem at hand. You could even mix inheritance and composition, for example :

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);
}

Granted, this last example is not clean (meaning, avoid it), but it shows how composition can be used.

Bottom line is that both examples, DataProcessor and Shape are "solid" classes, and their API should not change. However, the adapter classes may change and if they do, these changes should only affect their composing container, thus limit the maintenance to only these classes and not the entire application, as opposed to Example 1 where any change require more changes throughout the application. It all depends how flexible your application needs to be.



回答2:

If you would change Fruit.peel()'s return type, you would have to modify Apple.peel() as well. But you don't have to change Apple's interface.

Remember: The interface are only the method names and their signatures, NOT the implementation.

Say you'd change Fruit.peel() to return a boolean instead of a int. Then, you could still let Apple.peel() return an int. So: The interface of Apple stays the same but Fruit's changed.

If you would have use inheritance, that would not be possible: Since Fruit.peel() now returns a boolean, Apple.peel() has to return an boolean, too. So: All code that uses Apple.peel() has to be changed, too. In the composition example, ONLY Apple.peel()'s code has to be changed.



回答3:

The key word in the sentence is "interface".

You'll almost always need to change the Apple class in some way to accomodate the new return type of Fruit.peel, but you don't need to change its public interface if you use composition rather than inheritance.

If Apple is a Fruit (ie, inheritance) then any change to the public interface of Fruit necessitates a change to the public interface of Apple too. If Apple has a Fruit (ie, composition) then you get to decide how to accomodate any changes to the Fruit class; you're not forced to change your public interface if you don't want to.



回答4:

Return type of Fruit.peel() is being changed from int to Peel. This doesn't meant that the return type of Apple.peel() is being forced to change to Peel as well. In case of inheritance, it is forced and any client using Apple has to be changed. In case of composition, Apple.peel() still returns an integer, by calling the Peel.getPeelCount() getter and hence the client need not be changed and hence Apple's interface is not changed ( or being forced to be changed)



回答5:

Well, in the composition case, Apple.peel()'s implementation needs to be updated, but its method signature can stay the same. And that means the client code (which uses Apple) does not have to be modified, retested, and redeployed.

This is in contrast to inheritance, where a change in Fruit.peel()'s method signature would require changes all way into the client code.