Composite Pattern

문지은·2022년 2월 24일
0

디자인패턴

목록 보기
3/4

문제 상황


pancake house menu, diner menu와 그의 하위 메뉴인 dessert menu, cafe menu 까지 모두 출력하게 만들고 싶다! 이 때 각각의 menu는 모두 타입이 배열, List, HashMap으로 다른 상태.

이 때 iterator pattern을 이용해서 타입이 다른 메뉴들에 대한 출력은 추상화했지만 하위 메뉴까지 더욱 유연하게 처리하고 싶다면?

즉 요구 사항을 정리하면 다음과 같다.

  • 각각의 메뉴와 그에 속한 하위 메뉴를 집어넣을 수 있는 트리 구조
  • 모든 각 메뉴에 대해 돌아가면서 작업 처리 가능하고 편리한 구조
  • 메뉴들에 대해서 선택적으로 작업 처리할 수 있는 더욱 유연한 구조

이 때 composite pattern을 이용할 수 있다.

정의

객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층 구조로 만들 수 있다.
클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체를 똑같은 방법으로 다룰 수 있다.
위의 예시에 적용해보면 diner menu에 속한 dessert menu처럼 객체 안에 객체가 든 형태도 개별 객체와 똑같이 취급 가능하다! 개별 객체도 다른 객체가 들지 않은 복합 객체로 생각!

Component : 복합 객체 내의 모든 객체들에 대한 인터펭이스 정의
Leaf : Composite에서 지원하는 기능 구현
Composite : 자식이 있는 구성요소의 행동 정의 및 자식 구성요소 저장. Leaf와 관련된 기능 구현

Composite 안에 Component가 들어 있다.
Component는 두 종류로 나눌 수 있다 : Composite, Leaf
-> 재귀적인 구조
뿌리는 Composite. 마지막은 Leaf인 거꾸로 된 트리 구조

MenuComponent에서 MenuItem과 Menu에서 사용하는 메소드 정의
MenuItem과 Menu는 자신이 쓰는 메소드만 오버라이드해서 재정의

메뉴 구성 요소(Component)

public abstract class MenuComponent {
	// 메소드들은 하위 클래스에서 구현하지 않을 경우 기본적으로 오류 던지도록 구현
   
    // 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();
	}
  
    // MenuItem 관련 메소드
	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();
	}
}

메뉴 항목(Leaf)

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

메뉴(Composite)

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

웨이터(Client)

public class Waitress {
	MenuComponent allMenus;
 	
    // 다른 모든 메뉴를 포함하고 있는 최상위 메뉴 구성요소 넘겨주기
	public Waitress(MenuComponent allMenus) {
		this.allMenus = allMenus;
	}
 
 	// 최상위 메뉴 구성요소 출력하면 트리에 속한 모든 메뉴 출력 가능
	public void printMenu() {
		allMenus.print();
	}
}

테스트

public class MenuTestDrive {
	public static void main(String args[]) {
		MenuComponent pancakeHouseMenu = 
			new Menu("PANCAKE HOUSE MENU", "Breakfast");
		MenuComponent dinerMenu = 
			new Menu("DINER MENU", "Lunch");
		MenuComponent cafeMenu = 
			new Menu("CAFE MENU", "Dinner");
		MenuComponent dessertMenu = 
			new Menu("DESSERT MENU", "Dessert of course!");
		MenuComponent coffeeMenu = new Menu("COFFEE MENU", "Stuff to go with your afternoon coffee");
  
		MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
  
		allMenus.add(pancakeHouseMenu);
		allMenus.add(dinerMenu);
		allMenus.add(cafeMenu);
  		
        // pancakeHouseMenu
		pancakeHouseMenu.add(new MenuItem(
			"K&B's Pancake Breakfast", 
			"Pancakes with scrambled eggs and toast", 
			true,
			2.99));
		pancakeHouseMenu.add(new MenuItem(
			"Regular Pancake Breakfast", 
			"Pancakes with fried eggs, sausage", 
			false,
			2.99));
		pancakeHouseMenu.add(new MenuItem(
			"Blueberry Pancakes",
			"Pancakes made with fresh blueberries, and blueberry syrup",
			true,
			3.49));
		pancakeHouseMenu.add(new MenuItem(
			"Waffles",
			"Waffles with your choice of blueberries or strawberries",
			true,
			3.59));
		
        // dinerMenu
		dinerMenu.add(new MenuItem(
			"Vegetarian BLT",
			"(Fakin') Bacon with lettuce & tomato on whole wheat", 
			true, 
			2.99));
		dinerMenu.add(new MenuItem(
			"BLT",
			"Bacon with lettuce & tomato on whole wheat", 
			false, 
			2.99));
		dinerMenu.add(new MenuItem(
			"Soup of the day",
			"A bowl of the soup of the day, with a side of potato salad", 
			false, 
			3.29));
		dinerMenu.add(new MenuItem(
			"Hot Dog",
			"A hot dog, with saurkraut, relish, onions, topped with cheese",
			false, 
			3.05));
		dinerMenu.add(new MenuItem(
			"Steamed Veggies and Brown Rice",
			"Steamed vegetables over brown rice", 
			true, 
			3.99));
 
		dinerMenu.add(new MenuItem(
			"Pasta",
			"Spaghetti with marinara sauce, and a slice of sourdough bread",
			true, 
			3.89));
   
   		// dinerMenu에 dessertMenu 추가
		dinerMenu.add(dessertMenu);
  
  		// dessertMenu
		dessertMenu.add(new MenuItem(
			"Apple Pie",
			"Apple pie with a flakey crust, topped with vanilla icecream",
			true,
			1.59));
  
		dessertMenu.add(new MenuItem(
			"Cheesecake",
			"Creamy New York cheesecake, with a chocolate graham crust",
			true,
			1.99));
		dessertMenu.add(new MenuItem(
			"Sorbet",
			"A scoop of raspberry and a scoop of lime",
			true,
			1.89));

		// cafeMenu
		cafeMenu.add(new MenuItem(
			"Veggie Burger and Air Fries",
			"Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
			true, 
			3.99));
		cafeMenu.add(new MenuItem(
			"Soup of the day",
			"A cup of the soup of the day, with a side salad",
			false, 
			3.69));
		cafeMenu.add(new MenuItem(
			"Burrito",
			"A large burrito, with whole pinto beans, salsa, guacamole",
			true, 
			4.29));

		// cafeMenu에 coffeeMenu 추가
		cafeMenu.add(coffeeMenu);

		// coffeeMenu
		coffeeMenu.add(new MenuItem(
			"Coffee Cake",
			"Crumbly cake topped with cinnamon and walnuts",
			true,
			1.59));
		coffeeMenu.add(new MenuItem(
			"Bagel",
			"Flavors include sesame, poppyseed, cinnamon raisin, pumpkin",
			false,
			0.69));
		coffeeMenu.add(new MenuItem(
			"Biscotti",
			"Three almond or hazelnut biscotti cookies",
			true,
			0.89));
 
		Waitress waitress = new Waitress(allMenus);
   
		waitress.printMenu();		// print
	}
}

단일 역할 원칙(SRP) vs 투명성

컴포지트 패턴은 위의 예시에서 보듯이 Component에서 계층 구조를 관리하는 일, 메뉴와 관련된 작업을 처리하는 일 이렇게 두가지 일을 처리하기 때문에 단일 역할 원칙을 위반한다. 하지만 클라이언트가 해당 원소가 composite인지 leaf node 인지 투명하게 알 수 있기 때문에 투명성을 확보한 패턴이라고 볼 수 있다.
leaf 노드에서 composite 관련된 메서드를 호출하거나 재정의하는 등 의도하지 않게 동작하거나 instanceof 를 이용해서 검증을 한번 더 해야하는 단점이 있지만 leaf node를 자식이 없는 composite 객체로 생각한다면 문제가 없다.
-> 상황에 따라서 판단

Iterator Pattern

우리가 아직 해결하지 않은 요구 조건이 있다. 전체 트리 구조의 메뉴에서 선택적으로 원하는 메뉴들에 대해서도 작업 처리가 가능하도록 하는 것. 이를 위해서 Iterator Pattern을 사용해보자.

Menu

public class Menu extends MenuComponent {
	Iterator<MenuComponent> iterator = null;
    .
    .
    .

	// Menu에 CompositeIterator 추가
    // 현재 복합 객체에 대한 반복자 리턴
	public Iterator<MenuComponent> createIterator() {
		if (iterator == null) {
			iterator = new CompositeIterator(menuComponents.iterator());
		}
		return iterator;
	}
.
.
.

MenuItem

public class MenuItem extends MenuComponent {
 
.
.
.
	// NullItertor 추가
    // Leaf는 반복자가 존재하지 않으므로 아무 일도 하지 않는 null 반복자 리턴
	public Iterator<MenuComponent> createIterator() {
		return new NullIterator();
	}
 .
 .
 .

CompositeIterator

public class CompositeIterator implements Iterator<MenuComponent> {
	// 위의 코드에서는 leaf node 안에서 반복자를 써서 작업을 처리하고 composite인 경우 하위 메서드를 호출하여 반복
    // 하지만 해당 코드에서는 iterator를 사용하므로 외부에서 반복자 사용 -> 스택 사용해야 함
	Stack<Iterator<MenuComponent>> stack = new Stack<Iterator<MenuComponent>>();
   
   // 최상위 composite의 iterator 전달
	public CompositeIterator(Iterator<MenuComponent> iterator) {
		stack.push(iterator);
	}
   
	public MenuComponent next() {
		if (hasNext()) {
			Iterator<MenuComponent> iterator = stack.peek();
			MenuComponent component = iterator.next();
			stack.push(component.createIterator());
			return component;
		} else {
			return null;
		}
	}
  
	public boolean hasNext() {
		if (stack.empty()) {
			return false;
		} else {
			Iterator<MenuComponent> iterator = stack.peek();
			if (!iterator.hasNext()) {
				stack.pop();
				return hasNext();
			} else {
				return true;
			}
		}
	}
	
	/*
	 * No longer needed as of Java 8
	 * 
	 * (non-Javadoc)
	 * @see java.util.Iterator#remove()
	 *
	public void remove() {
		throw new UnsupportedOperationException();
	}
	*/
}

Waitress

public class Waitress {
	MenuComponent allMenus;
 
	public Waitress(MenuComponent allMenus) {
		this.allMenus = allMenus;
	}
 
	public void printMenu() {
		allMenus.print();
	}
  
	public void printVegetarianMenu() {
		Iterator<MenuComponent> iterator = allMenus.createIterator();

		System.out.println("\nVEGETARIAN MENU\n----");
		while (iterator.hasNext()) {	
        	// iterator 이용해서 채식주의자 메뉴만 출력
			MenuComponent menuComponent = iterator.next();
			try {
				if (menuComponent.isVegetarian()) {
					menuComponent.print();
				}
			} catch (UnsupportedOperationException e) {}
            // 여기서 try-catch를 사용한 이유
            // client에서 composite, leaf를 똑같이 취급하고 composite에서 지원하지 않는 메소드임을 나타내기 위해서 사용
		}
	}
}

한줄 요약

클라이언트는 leaf node인지 복합 객체인지 생각하지 않고 최상위 노드를 이용해서 트리 전체에 대한 작업 가능
-> 부분 - 객체 관계를 가진 객체 컬렉션을 모두 똑같은 방식으로 다루고 싶을 때 사용하자~

profile
백엔드 개발자입니다.

0개의 댓글