Composite

The Composite Pattern is used to model tree structures representing part-whole hierarchies, letting the client treats individual objects and compositions of objects in a uniform way.

The client refers to the class object of its request through the Component interface, that is implemented by all the objects in the composition. An element in the composition that has no children is a Leaf. The Composite is an element in the composition that do haves children.

The issue with this approach is that it violates the Single Responsibility Design Principle, since we put two responsibilities in one class and, worse, we rely on the fact that some methods shouldn't be used accordingly to the usage of the actual object.

The point is that we trade safety for transparency. In this way the client could treat composites and leaf operations at the same way; but it should take care of not mistaking one for the other.

As example of usage for the Composite Patter we see a rewrite of the menus management system we have seen when we talked about the Iterator Pattern. Suppose that we have a change request: we should be able to add sub-menus to a menu. In the specific case, we want that our dinner menu has associated a dessert menu.

To do that we decide to base our class hierarchy on the MenuComponent class in this way:
public abstract class MenuComponent { // 1
    // 2. composite operation methods
    public void add(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int i) {
        throw new UnsupportedOperationException();
    }

    // 3. operation methods
    public String getName() {
        throw new UnsupportedOperationException();
    }

    public String getDescr() {
        throw new UnsupportedOperationException();
    }

    public double getPrice() {
        throw new UnsupportedOperationException();
    }

    public boolean isVeg() {
        throw new UnsupportedOperationException();
    }

    // 4. common method
    public void print() {
        throw new UnsupportedOperationException();
    }
}
1. the class is abstract: we can't actually instantiate MenuComponent object.
2. add(), remove(), getChild() are methods for the composite branch of the hierarchy, the operation should not redefine them, giving an exception to the client if it wrongly try to use them.
3. getName(), getDescr(), getPrice(), and isVeg() are methods for the operation leaves. They don't make any sense for the component objects.
4. print() is a common method, should implemented for all the actual objects. In case of Composite, it should just call the print method for all the leaves associated to it.

In this new architecture, the MenuItem is implemented in this way:
public class MenuItem extends MenuComponent {
    private String name;
    private String descr;
    private boolean veg;
    private double price;

    public MenuItem(String name, String descr, boolean veg, double price) {
        this.name = name;
        this.descr = descr;
        this.veg = veg;
        this.price = price;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescr() {
        return descr;
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public boolean isVeg() {
        return veg;
    }

    @Override
    public void print() {
        System.out.print(" " + getName());
        if(isVeg()) {
            System.out.print("(v)");
        }
        System.out.print(", " + getPrice());
        System.out.println(" --- " + getDescr());
    }
}
Only the methods that actually make sense are implemented here; same structure for the class Menu:
public class Menu extends MenuComponent {
    ArrayList<MenuComponent> components = new ArrayList<MenuComponent>(); // 1
    String name;
    String descr;

    public Menu(String name, String descr) {
        this.name = name;
        this.descr = descr;
    }

    @Override
    public void add(MenuComponent mc) {
        components.add(mc);
    }

    @Override
    public void remove(MenuComponent mc) {
        components.remove(mc);
    }

    @Override
    public MenuComponent getChild(int i) {
        return components.get(i);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescr() {
        return descr;
    }

    @Override
    public void print() {
        System.out.print("\n" + getName());
        System.out.println(", " + getDescr());
        System.out.println("----------------");

        for(MenuComponent mc: components) { // 2
            mc.print();
        }
    }
}
1. we change a bit the Menu from the original implementation. Now it has a list of the owned sub-elements, and it keeps track of its own name and description.
2. here the print() implementation relies on the sub-elements print(). We use implicitly the Iterator Pattern via the handy Java 5 "for" construct.

Since we moved most of the logic for the management of the menus inside the relative classes - and it makes a lot of sense, thinking about it - the waitress class is now a lot simpler:
public class Waitress {
    MenuComponent menus;

    public Waitress(MenuComponent menus) {
        this.menus = menus;
    }

    public void printMenu() {
        menus.print();
    }
}
And here is the sample code for testing our new application:
MenuComponent menuPancake = new Menu("Pancake", "Breakfast");
MenuComponent menuDiner = new Menu("Diner", "Lunch");
MenuComponent menuCafe = new Menu("Café", "Dinner");
MenuComponent menuDessert = new Menu("Dessert", "After Dinner Dessert");

menuPancake.add(new MenuItem("M1 Pancake", "Sausage pancake", false, 2.99));
menuPancake.add(new MenuItem("V1 Pancake", "Veggie pancake", true, 2.99));
menuPancake.add(new MenuItem("V2 Pancake", "Blueberry pancake", true, 3.49));
menuPancake.add(new MenuItem("V3 Pancake", "Waffle", true, 3.49));
menuDiner.add(new MenuItem("V1 Diner", "Veggie stuff", true, 2.99));
menuDiner.add(new MenuItem("M1 Diner", "Hamburger", false, 2.99));
menuDiner.add(new MenuItem("M2 Diner", "Soup", false, 3.49));
menuDiner.add(new MenuItem("M3 Diner", "Hotdog", false, 3.99));
menuCafe.add(new MenuItem("M1 Cafè", "FatBurger", false, 4.99));
menuDessert.add(new MenuItem("D1 Cafè", "Apple Pie", true, 1.59));

MenuComponent menuAll = new Menu("All menus", "The complete offer");
menuAll.add(menuPancake);
menuAll.add(menuDiner);
menuAll.add(menuCafe);

menuCafe.add(menuDessert);

Waitress w = new Waitress(menuAll);
w.printMenu();
More on this pattern in chapter eight of Head First Design Patterns. This post is just the result of my reading and taking some memo from it.

No comments:

Post a Comment