반응형
반복자 패턴 이해를 위한 간단한 문제 제안
- 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는 구현방식 리스트이기때문에
#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로 넘기면 간단하게 문제가 해결됩니다.
- 그러나 갑자기 요구사항이 서브메뉴를 추가해달라는 요청이 들어옵니다.
- 서브메뉴라는것은 위의 그림처럼 메뉴안의 요소에 또 다른 메뉴를 넣을 수 있도록 구현하는것입니다.
- 이렇게 가능하기 위해서는 위의 그림과 같이 트리구조가 필요합니다.
- 메뉴, 서브메뉴, 메뉴 항목 등을 넣을 수 있는 트리 구조가 필요합니다.
- 각 메뉴의 모든 항목을 대상으로 특정 작업을 할 수 있는 방법이 제공되어야 합니다.
- 반복자 패턴만큼 편해야겠죠?
- 유연한 방법으로 반복 작업을 수행할 수 있어야합니다.
- 예를 들어 디저트 메뉴만 반복할 수 있어야하고 모든 메뉴를 반복할 수 도 있어야합니다.
컴포지트 패턴의 정의
- 객체를 트리구조로 구성해서 부분-전체 계층 구조로 구현하는것을 의미합니다.
- 컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.
- 위 예제에서는 메뉴가 있고 서브메뉴가 있는데 서브서브메뉴, 서브서브서브~메뉴가 있어도 하나의 복합 객체로 구성할 수 있습니다.
- 이런 점을 활용하여 컴포지트 패턴을 사용하면 간단한 코드로 하나의 복합 객체를 반복해서 사용할 수 있습니다.
컴포지트 패턴 구성도
컴포지트 패턴 구성도를 활용하여 예제 적용하기
개념 | 응용할 예제 객체 이름 |
Clinet | Waitress |
Component | MenuComponent |
Leaf | MenuItem |
Composite | Menu |
- 응용할 좌측 열의 개념들은 우측 개념들의 이름으로 응용하여 구현하는 방식으로 예제를 진행하도록 하겠습니다.
컴포지트 패턴 구현하기
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 |