Search code examples
oopcomposition

Composition, I don't quite get this?


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

Solution

  • 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.