디자인 패턴

CHAPTER 09.반복자 패턴과 컴포지트 패턴

반응형

반복자 패턴 이해를 위한 간단한 문제 제안

  • PancakeHouseMenu라는 식당과 DinerMenu라는 식당이 있습니다. 두 식당은 별개의 식당입니다.
  • 우연히 두 식당은 식당을 합치기로 했고 이 과정에서 서로 메뉴를 수정하기 싫어하는 신경전이 펼쳐지게 됩니다.
  • 메뉴 수정이 다른 이유는 아래처럼 PancakeHouseMenu식당과 DinerMenu 식당의 구현 방법이 다르기 때문입니다.
public class PancakeHouseMenu implements Menu {
	ArrayList<MenuItem> menuItems; // DinerMenu와 달리 PancakeHouseMenu는 배열을 통해 메뉴를 구현
 
	public PancakeHouseMenu() {
		menuItems = new ArrayList<MenuItem>();
    
		addItem("K&B's Pancake Breakfast", 
			"Pancakes with scrambled eggs and toast", 
			true,
			2.99);
 
		addItem("Regular Pancake Breakfast", 
			"Pancakes with fried eggs, sausage", 
			false,
			2.99);
 
		addItem("Blueberry Pancakes",
			"Pancakes made with fresh blueberries and blueberry syrup",
			true,
			3.49);
 
		addItem("Waffles",
			"Waffles with your choice of blueberries or strawberries",
			true,
			3.59);
	}

	public void addItem(String name, String description,
	                    boolean vegetarian, double price)
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		menuItems.add(menuItem);
	}
 
	public ArrayList<MenuItem> getMenuItems() {
		return menuItems;
	}
  
	public Iterator<MenuItem> createIterator() {
		return menuItems.iterator();
	}
  
	// other menu methods here
}
public class DinerMenu implements Menu {
	static final int MAX_ITEMS = 6;
	int numberOfItems = 0;
	MenuItem[] menuItems; // PancakeHouseMenu와 달리 DinerMenu는 배열을 통해 메뉴를 구현
  
	public DinerMenu() {
		menuItems = new MenuItem[MAX_ITEMS];
 
		addItem("Vegetarian BLT",
			"(Fakin') Bacon with lettuce & tomato on whole wheat", true, 2.99);
		addItem("BLT",
			"Bacon with lettuce & tomato on whole wheat", false, 2.99);
		addItem("Soup of the day",
			"Soup of the day, with a side of potato salad", false, 3.29);
		addItem("Hotdog",
			"A hot dog, with sauerkraut, relish, onions, topped with cheese",
			false, 3.05);
		addItem("Steamed Veggies and Brown Rice",
			"Steamed vegetables over brown rice", true, 3.99);
		addItem("Pasta",
			"Spaghetti with Marinara Sauce, and a slice of sourdough bread",
			true, 3.89);
	}
  
	public void addItem(String name, String description, 
	                     boolean vegetarian, double price) 
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		if (numberOfItems >= MAX_ITEMS) {
			System.err.println("Sorry, menu is full!  Can't add item to menu");
		} else {
			menuItems[numberOfItems] = menuItem;
			numberOfItems = numberOfItems + 1;
		}
	}
 
	public MenuItem[] getMenuItems() {
		return menuItems;
	}
  
	public Iterator<MenuItem> createIterator() {
		return new DinerMenuIterator(menuItems);
		//return new AlternatingDinerMenuIterator(menuItems);
	}
 
	// other menu methods here
}
  • 위에 처럼 메뉴를 나타내는 인스턴스를 리스트와 배열로 선언한 차이가 존재하는것이 핵심 문제입니다.
  • 이렇게 될 경우 음식점의 주문을 처리하는 종업원에 문제가 발생하게 됩니다.
  • 종업원이 수행해야 할 메소드는 위와 같이 이루어져있습니다.
  • 위와 같이 음식점에게 전달해야하는 메소드들이 존재하는데 인터페이스도 다른 상황에서는 음식점별로 객체를 생성해야합니다.
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayLIst<MenuItem> breakfastItems = pancakeHouseMenu.getMenuitems();

DinnerMenu dinerMenu = new DinerMenu();
MenuItem[] lunchItems = dinerMenuItems();
  • 나아가 먼저 구현 방식이 다르기 때문에 메소드 이름이 똑같아도 위와 같이 객체를 별도로 선언해야하는 문제가 있습니다.
  • 또한 위와 같이 순환 로직을 돌리가나 추가 기능을 사용시 위 처럼 음식점별로 순환문을 돌려 비슷한 로직을 작성하게 되는 일이 발생합니다.
  • 이러한 문제들은 같은 인터페이스로 통일시키는 방법을 통해 해결이 가능합니다.

반복자 패턴을 통해 문제 해결하기

  • 위에서 말한것처럼 이러한 문제를 해결하기 위해서는 인터페이스를 통일시켜야합니다.
  • 인터페이스를 통일 시키기위해서는 변경되는 부분을 캡슐화할 필요가있습니다.
#PancakeHouseMenu
for(int i = 0; i < breakfastItems.size(); i++){
	MenuItem menuItem = breakfasteItems.get(i);
}
#DinerMendu
for(int i = 0; i < breakfastItems.size(); i++){
	MenuItem menuItem = lunchItems[i];
}
  • 현재 변경되어야 할 부분은 위와 같습니다.
    • PancakeHouseMenu는 구현방식 리스트이기때문에 .get()을 사용해서 조회를 하고 DinnerMenu는 배열이기 때문에 []방식으로 조회하는것을 확인할 수 있습니다.
    • 이렇게 객체의 반복 작업 처리를 해결하기 위해 Iterator 객체를 사용하면 인터페이스를 통일 할 수 있습니다.
#PancakeHouseMenu
Iterator iterator = breakfastMenu.createIterator();

while(iterator.hasNext()){
	MenuITem menuItem = iterator.next();
}
#DinerMendu
Iterator iterator = lunchMenu.createIterator();

while(iterator.hasNext()){
	MenuITem menuItem = iterator.next();
}
  • 위와 같이 사용할 수 있도록 통일 시킨다면 종업원이 호출 하는 반복문 로직을 각 음식점마다 호출하는것이 아닌 음식점을 호출 하는 개념으로 통일시켜 코드의 중복성을 제거할 수 있습니다.

반복자패턴 구현하기

  • 반복자 패턴을 구현하여 클라이언트의 중복된 코드를 제거하기 위해서는 2가지 작업이 필요합니다.
  • 첫번째로는 인터페이스를 통일시켜야합니다.
    • 인터페이스를 통일시켜 클라이언트가 사용하게되는 인스턴스의 자료형을 통일시켜 중복된 코드를 제거해야합니다.
  • 두 번째로는 메소드를 통일시켜야 합니다.
    • 인터페이스를 통해 수행되는 메소드의 이름이 같아야만 중복된 코드를 제거할 수 있습니다.
  • 먼저 메소드의 이름부터 통일시키도록 하겠습니다.
public interface Iterator {
	boolean hasNext();
	MenuItem next();
}
  • 먼저 Iterator 인터페이스를 정의합니다.
public class DinerMenuIterator {
	MenuItem[] items;
	int position = 0;
 
	//(2)
	public DinerMenuIterator(MenuItem[] items) {
		this.items = items;
	}
 
	public MenuItem next() {
		MenuItem menuItem = items[position];
		position = position + 1;
		return menuItem;
		
		
		// or shorten to 
		return items[position++];
	}
 
	public boolean hasNext() {
		if (position >= items.length || items[position] == null) {
			return false;
		} else {
			return true;
		}
		
		// or shorten to
		return items.length > position;
	}
}
  • DinnerMenuIterator는 Iterator 인터페이스를 구현하는 반복되는 작업만을 가지는 클래스입니다.
  • (2) 부분에서는 반복 작업을 수행하기한 메뉴 배열을 인자로 받아들입니다.
    • PancakeHouseMenu에서는 리스트로 받습니다.
  • next(), hasNext() 메소드는 반복문을 수행하기 위한 메소드의 기능들을 의미합니다.
public class DinerMenu {
	static final int MAX_ITEMS = 6;
	int numberOfItems = 0;
	MenuItem[] menuItems;
  
	public DinerMenu() {
		menuItems = new MenuItem[MAX_ITEMS];
 
		addItem("Vegetarian BLT",
			"(Fakin') Bacon with lettuce & tomato on whole wheat", true, 2.99);
		addItem("BLT",
			"Bacon with lettuce & tomato on whole wheat", false, 2.99);
		addItem("Soup of the day",
			"Soup of the day, with a side of potato salad", false, 3.29);
		addItem("Hotdog",
			"A hot dog, with sauerkraut, relish, onions, topped with cheese",
			false, 3.05);
		addItem("Steamed Veggies and Brown Rice",
			"Steamed vegetables over brown rice", true, 3.99);
		addItem("Pasta",
			"Spaghetti with Marinara Sauce, and a slice of sourdough bread",
			true, 3.89);
	}
  
	public void addItem(String name, String description, 
	                     boolean vegetarian, double price) 
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		if (numberOfItems >= MAX_ITEMS) {
			System.err.println("Sorry, menu is full!  Can't add item to menu");
		} else {
			menuItems[numberOfItems] = menuItem;
			numberOfItems = numberOfItems + 1;
		}
	}
 
	public MenuItem[] getMenuItems() {
		return menuItems;
	}
  //(1)
	public Iterator createIterator() {
		return new DinerMenuIterator(menuItems);
		// To test Alternating menu items, comment out above line,
		// and uncomment the line below.
		//return new AlternatingDinerMenuIterator(menuItems);
	}
 
	// other menu methods here
}
  • (1)번 구간에서는 Iterator 인터페이스를 린턴합니다. 이후 클라이언트는 menuitem이 어떻게 관리되는지 모릅니다. 나아가 DinnerMenuIterator 클래스가 어떤식으로 구현되었는지도 모릅니다. 그럼에도 불구하고 클라이언트(종업원 클래스)는 menuItems 항목에 접근하여 반복문을 돌릴수 있습니다.
public class Waitress {
	Menu pancakeHouseMenu;
	Menu dinerMenu;
 
	public Waitress(
        PancakeHouseMenu pancakeHouseMenu, DinnerMenu dinerMenu) {
		this.pancakeHouseMenu = pancakeHouseMenu;
		this.dinerMenu = dinerMenu;
	}
 
	public void printMenu() {
		Iterator pancakeIterator = pancakeHouseMenu.createIterator();
		Iterator dinerIterator = dinerMenu.createIterator();

		System.out.println("MENU\n----\nBREAKFAST");
		printMenu(pancakeIterator);
		System.out.println("\nLUNCH");
		printMenu(dinerIterator);

	}
 
	private void printMenu(
        Iterator iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			System.out.print(menuItem.getName() + ", ");
			System.out.print(menuItem.getPrice() + " -- ");
			System.out.println(menuItem.getDescription());
		}
	}
	//기타 메소드
	public void printVegetarianMenu() {
		printVegetarianMenu(pancakeHouseMenu.createIterator());
		printVegetarianMenu(dinerMenu.createIterator());
	}
 
	public boolean isItemVegetarian(String name) {
		Iterator breakfastIterator = pancakeHouseMenu.createIterator();
		if (isVegetarian(name, breakfastIterator)) {
			return true;
		}
		Iterator dinnerIterator = dinerMenu.createIterator();
		if (isVegetarian(name, dinnerIterator)) {
			return true;
		}
		return false;
	}


	private void printVegetarianMenu(
        Iterator iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			if (menuItem.isVegetarian()) {
				System.out.print(menuItem.getName());
				System.out.println("\t\t" + menuItem.getPrice());
				System.out.println("\t" + menuItem.getDescription());
			}
		}
	}

	private boolean isVegetarian(String name, Iterator iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			if (menuItem.getName().equals(name)) {
				if (menuItem.isVegetarian()) {
					return true;
				}
			}
		}
		return false;
	}
}
  • 생성한 반복자 코드를 활용하기 위해서는 조업원 객체에 적용해야합니다.
  • 종업원에 반복자를 적용하는 방법은 생성자를 통해 생성시 주입을 해준뒤 printMenu()메소드를 사용해서 사용해주면 됩니다.

테스트용 코드

public class MenuTestDrive {
	public static void main(String args[]) {
		PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
		DinerMenu dinerMenu = new DinerMenu();
		Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
		// Use implicit iteration
		waitress.printMenu();
	}
}

실행결과

public class PancakeHouseMenu implements Menu {
	ArrayList<MenuItem> menuItems;
 
	public PancakeHouseMenu() {
		menuItems = new ArrayList<MenuItem>();
    
		addItem("K&B's Pancake Breakfast", 
			"Pancakes with scrambled eggs, and toast", 
			true,
			2.99);
 
		addItem("Regular Pancake Breakfast", 
			"Pancakes with fried eggs, sausage", 
			false,
			2.99);
 
		addItem("Blueberry Pancakes",
			"Pancakes made with fresh blueberries, and blueberry syrup",
			true,
			3.49);
 
		addItem("Waffles",
			"Waffles, with your choice of blueberries or strawberries",
			true,
			3.59);
	}

	public void addItem(String name, String description,
	                    boolean vegetarian, double price)
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		menuItems.add(menuItem);
	}
 
	public ArrayList<MenuItem> getMenuItems() {
		return menuItems;
	}
  
	public Iterator<MenuItem> createIterator() {
		return menuItems.iterator();
	}
  
	// other menu methods here
}
  • 여기서부터는 PancakeHouseMenu의 인터페이스 메소드를 통일해보겠습니다.
  • PancakeHouseMenu는 별도의 반복자 인터페이스를 만들 필요가 없습니다.
    • DinnerMenuIterator와 같은 위치의 클래스를 만들 필요가 없다는 의미입니다.
  • 왜냐하면 PancakeHouseMenu는 리스트로 구현되어있는데 리스트의 상속정의 타입을 확인해보면 자바의 Iterator을 상속하고 있기 때문에 createIterator() 메소드 안의 menuItems.iterator()과 같이 사용할 수 있기 때문입니다.

반복자 패턴 예제 중간 UML 구 조

  • 지금까지 구현한 내용들을 정리한 UML 입니다.
  • 여기까지 메소드를 동일하게 묶는 작업들은 진행했습니다.
  • 이후에는 인터페이스를 같이 묶는 작업들을 진행해서 Waitress의 구상 메뉴 클래스 의존성을 제거하여 하나의 인터페이스를 유지하는 작업을 진행하겠습니다.

인터페이스 통일하기

public interface Menu {
	public Iterator<MenuItem> createIterator();
}
  • 식당들의 인터페이스를 묶기 위해서 위와 같은 인터페이스를 선언해주도록 하겠습니다.
  • 주문 메뉴는 클라이언트에서 선언해서 추가하도록 설정하도록 가정하여 진행하므로 별도의 인터페이스는 추가하지 않겠습니다.
public class PancakeHouseMenu implements Menu {
	...
}
public class DinnerMenu implements Menu {
	...
}
  • 음식점 객체들을 Menu 인터페이스로 통일합니다.
public class Waitress {
	Menu pancakeHouseMenu;
	Menu dinerMenu;
 
//(1)
	public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
		this.pancakeHouseMenu = pancakeHouseMenu;
		this.dinerMenu = dinerMenu;
	}
	
	// implicit iteration
	public void printMenu() {
		List<MenuItem> breakfastItems = ((PancakeHouseMenu) pancakeHouseMenu).getMenuItems();
		for (MenuItem m : breakfastItems) {
			printMenuItem(m);
		}
		
		MenuItem[] lunchItems = ((DinerMenu) dinerMenu).getMenuItems();
		for (MenuItem m : lunchItems) {
			printMenuItem(m);
		}
	}
	

	public void printMenuItem(MenuItem menuItem) {
		System.out.print(menuItem.getName() + ", ");
		System.out.print(menuItem.getPrice() + " -- ");
		System.out.println(menuItem.getDescription());
	}
}
  • Waitress 객체의 생성자 부분을 리팩토링하겠습니다.
  • (1)번 구간 코드를 확인해보면 이전 Waitress객체와 다르게 자료형(구상클래스)을 Menu 인터페이스로 통일한것을 확인할 수 있습니다.
  • 이를 통해 인터페이스를 통일하였습니다.

반복자 패턴 예제 최종 UML 구 조

반복자 패턴의 정의

  • 반복자 패턴은 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공하는 패턴입니다.
  • 이런한 방법론은 컬렉션 객체 안에 들어있는 항목에 접근하는 방식의 통일성을 통해 다형적인 코드를 생산할 수 있습니다.
  • 이런 패턴은 SRP 규칙을 준수하고 있습니다.
    • 반복하는 일은 반복자 객체가 책임지고 이로 인해 집합체 인터페이스의 구현이 간단해지게됩니다.

반복자 패턴 구조

단일 역할 원칙

  • 단일 역할 원칙의 핵심 키워드는 어떤 클래스의 바뀌는 이유는 하나뿐이어야 한다는것입니다.
  • 만약 하나의 클래스에 변경요인이 많다면 반복자 관련 기능이 바뀌었을때 클래스가 변경되어야 하는데 변경요인이 많은만큼 반복자 관련 기능이 변경될 확률이 높아지게 됩니다.
  • 그러므로 항상 클래스의 변경 요인은 한 가지가 아닌지 고민하면서 코드를 구현해야합니다.

+ 추가 문제 제안

  • 현재 상태에서 이번에는 Cafe 메뉴도 결합하라는 요구사항이 발생하게 되었습니다.
public class CafeMenu {
	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);
	}
 
	...
}
  • 위와 같은 상태이고 CafeMenu는 HashMap 형태로 구현되어있습니다.
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();
	}
}
  • 새로운 메뉴를 추가하는 요구사항을 충족하기 위한 리팩토링을 진행하도록 해보겠습니다.
  • 먼저 같은 인터페이스를 묶기 위해 Menu 인터페이스를 정의하도록 진행하였습니다.
  • HashMap은 Collection을 상속하기에 Collection이 Iterator를 상속하는 점을 활용하여 menuItems.values().iterator()을 통해 Iterator를 반환하도록 구현하였습니다.
public class Waitress {
	Menu pancakeHouseMenu;
	Menu dinerMenu;
	Menu cafeMenu; //(1)
 
	public Waitress(Menu pancakeHouseMenu, Menu dinerMenu, Menu cafeMenu) {
		this.pancakeHouseMenu = pancakeHouseMenu;
		this.dinerMenu = dinerMenu;
		this.cafeMenu = cafeMenu; //(2)
	}
 
	public void printMenu() {
		Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
		Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
		Iterator<MenuItem> cafeIterator = cafeMenu.createIterator(); //(2)

		System.out.println("MENU\n----\nBREAKFAST");
		printMenu(pancakeIterator);
		System.out.println("\nLUNCH");
		printMenu(dinerIterator);
		System.out.println("\nDINNER"); //(3)
		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()); //(4)
		}
	}

 //기타 메소드
	public void printVegetarianMenu() {
		System.out.println("\nVEGETARIAN MENU\n---------------");
		printVegetarianMenu(pancakeHouseMenu.createIterator());
		printVegetarianMenu(dinerMenu.createIterator());
		printVegetarianMenu(cafeMenu.createIterator());
	}
 
	public boolean isItemVegetarian(String name) {
		Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
		if (isVegetarian(name, pancakeIterator)) {
			return true;
		}
		Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
		if (isVegetarian(name, dinerIterator)) {
			return true;
		}
		Iterator<MenuItem> cafeIterator = cafeMenu.createIterator();
		if (isVegetarian(name, cafeIterator)) {
			return true;
		}
		return false;
	}


	private void printVegetarianMenu(Iterator<MenuItem> iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			if (menuItem.isVegetarian()) {
				System.out.print(menuItem.getName() + ", ");
				System.out.print(menuItem.getPrice() + " -- ");
				System.out.println(menuItem.getDescription());
			}
		}
	}

	private boolean isVegetarian(String name, Iterator<MenuItem> iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			if (menuItem.getName().equals(name)) {
				if (menuItem.isVegetarian()) {
					return true;
				}
			}
		}
		return false;
	}
}
  • (1)번에서 CafeMenu 인스턴스를 선언해주었습니다.
  • (2)번에서 Waitress 객체 생성시 cafeMenu도 생성 조건으로 재정의해주었습니다.
  • (3),(4)번에서 cafeMenu를 호출하기 위한 적업을 진행합니다.

테스트

public class MenuTestDrive {
	public static void main(String args[]) {
		PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
		DinerMenu dinerMenu = new DinerMenu();
		CafeMenu cafeMenu = new CafeMenu();
 
		Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu, cafeMenu);
 
		waitress.printMenu();
		waitress.printVegetarianMenu();

		System.out.println("\nCustomer asks, is the Hotdog vegetarian?");
		System.out.print("Waitress says: ");
		if (waitress.isItemVegetarian("Hotdog")) {
			System.out.println("Yes");
		} else {
			System.out.println("No");
		}
		System.out.println("\nCustomer asks, are the Waffles vegetarian?");
		System.out.print("Waitress says: ");
		if (waitress.isItemVegetarian("Waffles")) {
			System.out.println("Yes");
		} else {
			System.out.println("No");
		}
	}
}

수행결과

컴포지트 패턴 이해를 위한 새로운 문제 제안

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();
//(1) 메뉴를 계속 추가해줘야한다.		
		Iterator<MenuItem> cafeIterator = cafeMenu.createIterator(); 

		System.out.println("MENU\n----\nBREAKFAST");
		printMenu(pancakeIterator);
		System.out.println("\nLUNCH");
		printMenu(dinerIterator);
		System.out.println("\nDINNER"); //(3) 호출도 계속 추가해줘야한다.
		printMenu(cafeIterator);
	}
...
  • 반복자 패턴을 활용해서 중복코드를 제거하는 방향으로 리팩토리 하였지만 메뉴를 추가할 때 마다 Waitress 객체에 printMenu에 수정이 계속 발생하는 문제가 발생합니다.
public class Waitress {
	List<Menu> menus;

	public Waitress(List<Menu> menu) {
		this.menus = menus;
	}
...
  • 위와 같이 waitress 객체의 생성자를 List로 넘기면 간단하게 문제가 해결됩니다.
  • 그러나 갑자기 요구사항이 서브메뉴를 추가해달라는 요청이 들어옵니다.
  • 서브메뉴라는것은 위의 그림처럼 메뉴안의 요소에 또 다른 메뉴를 넣을 수 있도록 구현하는것입니다.
  • 이렇게 가능하기 위해서는 위의 그림과 같이 트리구조가 필요합니다.
    • 메뉴, 서브메뉴, 메뉴 항목 등을 넣을 수 있는 트리 구조가 필요합니다.
    • 각 메뉴의 모든 항목을 대상으로 특정 작업을 할 수 있는 방법이 제공되어야 합니다.
      • 반복자 패턴만큼 편해야겠죠?
    • 유연한 방법으로 반복 작업을 수행할 수 있어야합니다.
      • 예를 들어 디저트 메뉴만 반복할 수 있어야하고 모든 메뉴를 반복할 수 도 있어야합니다.

컴포지트 패턴의 정의

  • 객체를 트리구조로 구성해서 부분-전체 계층 구조로 구현하는것을 의미합니다.
  • 컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.
  • 위 예제에서는 메뉴가 있고 서브메뉴가 있는데 서브서브메뉴, 서브서브서브~메뉴가 있어도 하나의 복합 객체로 구성할 수 있습니다.
    • 이런 점을 활용하여 컴포지트 패턴을 사용하면 간단한 코드로 하나의 복합 객체를 반복해서 사용할 수 있습니다.

컴포지트 패턴 구성도

컴포지트 패턴 구성도를 활용하여 예제 적용하기

개념응용할 예제 객체 이름
ClinetWaitress
ComponentMenuComponent
LeafMenuItem
CompositeMenu
  • 응용할 좌측 열의 개념들은 우측 개념들의 이름으로 응용하여 구현하는 방식으로 예제를 진행하도록 하겠습니다.

컴포지트 패턴 구현하기

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추상 클래스를 생성하도록 해보겠습니다.
  • MenuComponent는 MenuItem(잎)과 Menu(복합 노드) 모두에서 사용되는 인터페이스입니다.
  • 현재 MenuComponent는 Menu, MenuItem 두 개의 기능을 하고 있습니다.
    • 그렇기 때문에 재정의 하지 않는 메소드에는 기본적은 UnsupportedException 클래스를 반환하도록 정의하는 방식으로 구현합니다.
    • “한 추상 클래스가 두 기능을 담당하여 SRP를 어기고 있고 이로 인한 MenuItem은 MenuComponent 를 사용하는 것이 부적합하지 않은가?” 라는 의견이 나올수 있습니다.
    • 이런 주장들은 일리가 있지만 관리의 편의성과 MenuItem을 바라보는 관점의 차이로 해명이 가능합니다. MenuItem도 관리하는 잎 객체가 0개인 복합 노드로 볼 수도 있기 때문에 MenuComponent 추상 클래스를 사용할 수 있기 때문입니다.
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());
	}
}
  • MenuItem 구현입니다. MenuItem은 MenuComponet 인터페이스중 add(), remove(), getChild()를 제외한 메소드를 재정의하여 구현합니다.
    • 잎 역할을 하는 MenuItem은 MenuItem을 더하는 기능인 add(), remove(), getChild()를 사용할 이유가 없기 때문입니다.
    • 만약 사용할려고 한다면 기본적으로 정의 되어있는 UnSupportedException이 발생할 것입니다.
public class Menu extends MenuComponent {
//Menu에서는 MenuComponet 형식의 자식을 저장할 수 있습니다.
	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;
	}
 
//자신이 포함하는 모든 자식을 재귀적으로 돌면서 print()를 수행한다.
	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();
		}
	}
}
  • MenuItem을 관리하는 복합 객체인 Menu 클래스 구현입니다.
  • MenuItem과 다르게 MenuItem을 더하거나 뺄수 있는 add(), remove(), getChild()를 정의합니다.
public class Waitress {
	MenuComponent allMenus;
 
	public Waitress(MenuComponent allMenus) {
		this.allMenus = allMenus;
	}
 
	public void printMenu() {
		allMenus.print();
	}
}
  • Client인 Waitress 클래스 구현입니다.
  • 단순히 생성 시점에 모든 Menu를 포함하고 있는 최상위 메뉴 구성 요소만 넘겨주면 됩니다.
  • 전체 계층구조를 출력하고 싶다면 printMen() 메소드만 수행해주면 됩니다.
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();
	}
}
  • 마지막으로 테스트 수행을 위한 코드입니다

수행결과

정리

  • 반복자 패턴은 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공하는 디자인 패턴입니다.
  • 반복자(iterator)를 사용하면 내부구조를 드러내지 않으면서 클라이언트가 컬렉션 안에 들어있는 모든 원소에 접근하도록 할 수 있습니다.
  • 반복자 패턴을 사용하면 집합체를 대상으로 하는 반복작업을 별도의 객체로 캡슐화할 수 있습니다.
  • 반복자 패턴을 사용하면 컬렉션에 있는 모든 데이터를 대상으로 반복 작업을 하는 역할을 컬렉션에서 분리할 수 있습니다.
  • 반복자 패턴을 쓰면 반복 작업에 똑같은 인터페이스를 적용할 수 있으므로 집합체에 있는 객체를 활용하는 코드를 만들때 다형성을 활용할 수 있습니다.
  • 한 클래스에는 한 가지 역할만 부여하는것이 좋습니다(SRP)
  • 컴포지트 패턴은 객체를 트리구조로 구성해서 부분-전체 계층구조를 구현하는 디자인패턴입니다. 컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.
  • 컴포지트 패턴은 개별 객체와 복합 객체를 모두 담아 둘 수 있는 구조를 제공합니다.
  • 컴포지트 패턴을 사용하면 클라이언트가 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.
  • 복합 구조에 들어있는 것을 구성 요소라고 부릅니다. 구성 요소에는 복합 객체와 잎 객체가 존재합니다.
반응형

'디자인 패턴' 카테고리의 다른 글

CHAPTER 11.프록시 패턴  (0) 2023.05.06
CHAPTER 10.상태 패턴  (0) 2023.05.06
CHAPTER 08.템플릿 메소드 패턴  (0) 2023.05.06
CHAPTER 07.어댑터 패턴과 퍼사드 패턴  (2) 2023.05.06
CHAPTER 06.커멘드 패턴  (0) 2023.05.06