본문 바로가기
OOP/<헤드 퍼스트 디자인 패턴>, 에릭 프리먼 외

Composite

by 민휘 2023. 2. 24.

핵심 의도

클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있다. 개별 객체와 복합 객체는 서로 포함 관계인데, 객체를 트리구조로 구성해서 부분-전체 계층 구조를 구현한다.

 

적용 상황

계층 구조가 있는 자료구조의 아이템에 대해 공통된 인터페이스로 접근할 때 사용한다. 컴포지트 패턴을 사용하면 클라이언트에게는 계층 구조가 보이지 않기 때문에 클라이언트의 사용 코드가 매우 단순해진다는 장점이 있다.

 

솔루션의 구조와 각 요소의 역할

객체에게 책임을 분할하기

개별 객체(leaf)와 복합 객체(composite)가 존재하는 상황. 클라이언트는 개별 객체와 복합 객체를 동일한 방법으로 사용하고자 하므로 개별 객체와 복합 객체를 공통으로 묶는 인터페이스인 Component를 둔다. Component는 객체가 원래 가진 책임과 함께 계층구조를 관리하는 책임을 가진다.

구현 포인트

Composite와 반복자

Composite는 복합 객체이다. 복합 객체는 자식으로 개별 객체를 두거나 또다른 복합 객체를 둘 수 있다. 따라서 Composite는 개별객체와 복합객체를 둘다 가질 수 있는 컬렉션을 가진다. Component 타입의 컬렉션을 구성으로 두면 된다. 복합 객체가 자신의 모든 자식들에 접근하려고 할 때, 컬렉션이 제공하는 Iterator이 있다면 향상된 for 순환문을 사용할 수 있다.

 

Component 메소드 구현

모든 구성 요소는 Component 인터페이스를 구현한다. 하지만 잎과 노드는 각각 역할이 다르므로, 모든 메소드에 알맞는 기본 메소드 구현은 불가능하다. 그래서 자기 역할에 맞지 않는 상황을 기준으로 예외를 던지는 코드를 기본 구현으로 제공할 수 있다.

 

SRP 위반

Component는 객체가 원래 가진 책임과 함께 계층구조를 관리하는 책임을 가진다. 이는 단일 책임 원칙을 위반한다. 하지만 기능을 전부 넣어서 클라이언트가 복합 객체와 단일 객체를 똑같은 방식으로 처리할 수 있도록 만들 수 있다. 클라이언트는 어떤 원소가 복합 객체고 단일 객체인지 투명하게 볼 수 있다. 그럼에도 동일한 인터페이스를 사용해 접근할 수 있다.

만약 책임을 분리하여 계층구조 관리 책임을 다른 인터페이스로 분리한다면, 클라이언트는 조건문이나 Instanceof 연산자 등을 사용해서 단일 객체와 복합 객체를 구분하여 접근해야 한다. 하지만 책임을 확실하게 분리했으므로 부적절한 메소드 호출을 하지 않을 것이고 실행 중에 문제가 발생하지 않을 것이다.

상황에 따라 원칙을 적절하게 사용해야 한다. SRP를 위반하여 내부 구조를 공개하더라도, 클라이언트가 동일한 인터페이스로 간단하게 접근할 필요성이 더 크다면 컴포지트 패턴처럼 투명성을 확보하게 할 수 있다.

 

적용 예시

 

요구사항

Diner 식당의 메뉴에는 여러 단일 식사 메뉴들이 있다. 여기에 디저트 메뉴 집단을 Diner 식당 메뉴의 서브 메뉴로 추가하려고 한다. 클라이언트는 여전히 식당의 서브 메뉴와 단일 메뉴들을 동일한 인터페이스로 접근한다.

 

설계

식당의 서브 메뉴와 단일 메뉴는 계층 구조를 가진다. 단일 메뉴는 MenuItem가, 서브 메뉴는 Menu가 구현한다. 그리고 클라이언트가 이 계층 구조에 접근할 수 있도록 All Menus라고 하는 최상위 객체를 둔다. 그리고 서브 메뉴와 단일 메뉴에 접근할 때는 재귀적으로 접근한다. 서브 메뉴는 자식으로 단일 메뉴 혹은 서브 메뉴를 가지기 때문이다.

 

코드

MenuComponent

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

MenuItem(단일 객체)

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

Menu(복합 객체)

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

Waitress(Client)

public class Waitress {
	MenuComponent allMenus;
 
	public Waitress(MenuComponent allMenus) {
		this.allMenus = allMenus;
	}
 
	public void printMenu() {
		allMenus.print();
	}
}

MenuTestDrive

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

'OOP > <헤드 퍼스트 디자인 패턴>, 에릭 프리먼 외' 카테고리의 다른 글

Proxy  (1) 2023.02.24
State  (0) 2023.02.24
Iterator  (0) 2023.02.24
Template Method  (0) 2023.02.24
Facade  (0) 2023.02.24