Composite Pattern

Hyung Jun·2020년 12월 4일
0

Design Pattern

목록 보기
7/10
post-thumbnail

This article is lecture note from "Head First Design Patterns" by Elisabeth Freeman and Kathy Sierra and CS course from Kookmin by Inkyu Kim

Definition

The Composite Pattern allows you to compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

It does make sense when the core model of you app can be represented as a tree.😃

Problem

From the story of Iterator Pattern we dealt with before, imagine the situation that two restuarants get merged so we need to reconstruct our new menu applicaation.

Let's see the code first.

<CafeMenu.java>

public class CafeMenu implements Menu {
	HashMap<String, MenuItem> menuItems = new HashMap<String, MenuItem>();
  
	public CafeMenu() {
		addItem("Veggie Burger and Air Fries",
			"Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
			true, 3.99);
		addItem("Soup of the day",
			"A cup of the soup of the day, with a side salad",
			false, 3.69);
		addItem("Burrito",
			"A large burrito, with whole pinto beans, salsa, guacamole",
			true, 4.29);
	}
 
	public void addItem(String name, String description, 
	                     boolean vegetarian, double price) 
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		menuItems.put(name, menuItem);
	}
 
	public Map<String, MenuItem> getItems() {
		return menuItems;
	}
  
	public Iterator<MenuItem> createIterator() {
		return menuItems.values().iterator();
	}
}

Here is our CafeMenu class. It stores menu items in a hashmap. The menu items are initialised in the constructor.
We can see that wew implement the createIterator() method. Notice we're not getting an Iterator for the whole Hashmap, just for the values.
In menuItems.values().iterator(), we get the values of the haspMap first, which is just a collection of all the objects in the hashMap. Then luckily that collection supports the iterator() method, which returns a object of type java.util.iterator.

<Waitress.java>

public class Waitress {
	Menu pancakeHouseMenu;
	Menu dinerMenu;
	Menu cafeMenu;
 
	public Waitress(Menu pancakeHouseMenu, Menu dinerMenu, Menu cafeMenu) {
		this.pancakeHouseMenu = pancakeHouseMenu;
		this.dinerMenu = dinerMenu;
		this.cafeMenu = cafeMenu;
	}
 
	public void printMenu() {
		Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
		Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
		Iterator<MenuItem> cafeIterator = cafeMenu.createIterator();

		System.out.println("MENU\n----\nBREAKFAST");
		printMenu(pancakeIterator);
		System.out.println("\nLUNCH");
		printMenu(dinerIterator);
		System.out.println("\nDINNER");
		printMenu(cafeIterator);
	}
 
	private void printMenu(Iterator<MenuItem> iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			System.out.print(menuItem.getName() + ", ");
			System.out.print(menuItem.getPrice() + " -- ");
			System.out.println(menuItem.getDescription());
		}
	}
}

We're using the Cafe's menu for our dinner menu. All we have to do to print it is create the iterator, and pass it to printMenu().

Say you decide to create an ordering system that uses these classes. As you can see in the above Waitress class we saw three calls to printMenu and everytime we add or remove a menu we're going to open this code up for changes.

We need Composite Pattern !!😃

Structure

The Component interface describes operations that are common to both simple and complex elements of the tree.
The Leaf is a basic element of a tree that doesn't have sub-elements. Usually, leaf components end up doing most of the real work, since they don't have anyone to delegate the work to. It does this by implementing the operations the Composite supports.
The Container (aka composite*) is a element that has sub-elements: leaves or other containers. A container doesn't know the concrete classes of its children. It works with all sub-elements only via the component interface.
Upon receiving a request, a container delegates the work to its sub-elements, processes intermediate results and then returns the final result to the client.
The Client works with all elements through the component interface. As a result, the client can work in the same way with both simple or complex elements of the tree.

Source Code

<MenuComponent.java>

public abstract class MenuComponent {
   
	public void add(MenuComponent menuComponent) {
		throw new UnsupportedOperationException();
	}
	public void remove(MenuComponent menuComponent) {
		throw new UnsupportedOperationException();
	}
	public MenuComponent getChild(int i) {
		throw new UnsupportedOperationException();
	}
  
	public String getName() {
		throw new UnsupportedOperationException();
	}
	public String getDescription() {
		throw new UnsupportedOperationException();
	}
	public double getPrice() {
		throw new UnsupportedOperationException();
	}
	public boolean isVegetarian() {
		throw new UnsupportedOperationException();
	}
  
	public void print() {
		throw new UnsupportedOperationException();
	}
}

MenuComponent provides default implementation for every method. We mentioned that the component is interface but in this case we used abstract class instead.
Because some of these methods only make sense for MenuItems, and some only make sense for Menus, the default implementation is UnsupportedOperationException. That way, if MenuItem or Menu doesn't support an operation, they don't have to do anything, they can just inherit the default implementation.

<MenuItem.java>

public class MenuItem extends MenuComponent {
	String name;
	String description;
	boolean vegetarian;
	double price;
    
	public MenuItem(String name, 
	                String description, 
	                boolean vegetarian, 
	                double price) 
	{ 
		this.name = name;
		this.description = description;
		this.vegetarian = vegetarian;
		this.price = price;
	}
  
	public String getName() {
		return name;
	}
  
	public String getDescription() {
		return description;
	}
  
	public double getPrice() {
		return price;
	}
  
	public boolean isVegetarian() {
		return vegetarian;
	}
  
	public void print() {
		System.out.print("  " + getName());
		if (isVegetarian()) {
			System.out.print("(v)");
		}
		System.out.println(", " + getPrice());
		System.out.println("     -- " + getDescription());
	}
}

First we need to extend the MenuComponent interface. The constructor just takes the name, description, etc and keeps a reference to them all. This is pretty much like our old menu item implementation.
Look at the print() method. It is different from the previous implementation. Here we're overriding the print() method in the MenuComponent class. For MenuItem this method prints the complete menu entry: name, description, price and whether or not it is veggie.

<Menu.java>

import java.util.Iterator;
import java.util.ArrayList;

public class Menu extends MenuComponent {
	ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
	String name;
	String description;
  
	public Menu(String name, String description) {
		this.name = name;
		this.description = description;
	}
 
	public void add(MenuComponent menuComponent) {
		menuComponents.add(menuComponent);
	}
 
	public void remove(MenuComponent menuComponent) {
		menuComponents.remove(menuComponent);
	}
 
	public MenuComponent getChild(int i) {
		return (MenuComponent)menuComponents.get(i);
	}
 
	public String getName() {
		return name;
	}
 
	public String getDescription() {
		return description;
	}
 
	public void print() {
		System.out.print("\n" + getName());
		System.out.println(", " + getDescription());
		System.out.println("---------------------");
  
		Iterator<MenuComponent> iterator = menuComponents.iterator();
		while (iterator.hasNext()) {
			MenuComponent menuComponent = 
				(MenuComponent)iterator.next();
			menuComponent.print();
		}
	}
}

Menu is also a MenuComponent, just like MenuItem. Menu can have any number of children of type MenuComponent, we'll use an internal ArrayList to hold these.

There is a very importnat thing is on the print() method above.
We get to use an Iterator. We use it to iterate through all the Menu's components... those could be tother Menus, or they could be MenuItems. Since both Menus and MenuItems implement print(), we just call print() and the rest is up to them.

Note that if during this iteration, we encounter another Menu object, its print() method will start another iteration, and so on.

Why is it good?🧐

The Composite Pattern provides you with two basic element types that share a common interface: simple leaves and complex containers. A container can be composed of both leaves and other containers. This lets you construct a nested recursive object structure that resembles a tree. So it is useful to use the Composite Pattern when you have to implement a tree-like object structure.
This pattern makes you can work with complex tree structures more conveniently: use polymorphism and recursion to your advantage.

All elements defined by the Composite Pattern share a common interface. Using this interface, the client doesn't have to worry about the concrete class of the objects it works with.

Open Closed Principle. You can introduce new element types into the app without breaking the existing code, which now works with the object tree.

Composite & Decorator

Both have similar structure diagrams since both rely on recursive composition to organise an open-ended number of objects.

A Decorator is like a Composite but only has one child component. There's another significnat difference: Decorator adds additional responsibilities to the wrapped object, while Composite just "sums up" its children's results.

However, the patterns can also cooperate: you can use Decorator to extend the behaviour of a specific object in the Composite tree.

Implementation Summary

  • Step1
    Make sure that the core model of you app can be represented as a tree structure. Try to break into simple elements and containers. Remember that containers must be able to contain both simple elements and other containers.
  • Step2
    Declare the component interface with a list of methods that make sense for both simple and comoplex components.
  • Step3
    Create a leaf class to represent simple elements. A program may have multiple different leaf classes.
  • Step4
    Create a container class to represent complex elements. In this clas, provide an array field for storing references to sub-elements. The array must be able to store both leaves and containers, so make sure it's declared with the component interface type.
    While implementing the methods of the compnent interface, remember that a container is supposed to be delegating most of the work to sub-elements.
  • Step5
    Finally, define the methods for adding and removal of child elemetns in the container. Keep in mind that these operations can be declared in the component interface. This would violate the Interface Segregation Principle because the methods will be empty in the leaf class. However, the client will be able to treat all the elements equally, even when composing the tree.
profile
Keep Learning👨🏻‍💻

0개의 댓글