이터레이터와 컴포지트 패턴
이터레이터 패턴의 정의
- 이터레이터 패턴은 컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해주는 방법을 제공한다.
-
이 패턴을 이용하면 집합체 내에서 어떤 식으로 일이 처리되는지에 대해서 전혀 모르는 상태에서 그 안에 들어있는 모든 항목들에 대해서 반복작업을 수행할 수 있다. 컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식이 통일되어 있으면 어떤 종류의 집합체에 대해서도 사용할 수 있는 다형적인 코드를 만들 수 있기 때문이다.
- 쉽게 설명하면 컬렉션들을 공통된 인터페이스를 구현하게 함으로써 공통된 방법으로 모든 항목에 접근할 수 있는 방법이다.
이터레이터를 사용하지 않을 경우 문제 예
- MenuItem이라는 클래스로 하나의 메뉴를 구현한다.
- PancakeHouseMenu는 List로 Diner는 배열을 이용해서 구현하였다.
package com.kws.iterate;
public class MenuItem {
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 boolean isVegetarian() {
return vegetarian;
}
public double getPrice() {
return price;
}
}
package com.kws.iterate;
import java.util.ArrayList;
public class PancakeHouseMenu {
ArrayList<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
addItem("K&B 팬케이크 세트",
"스크램블드 에그와 토스트가 곁들여진 팬케이크",
true,
2.99);
addItem("레귤러 팬케이크 세트",
"달걀 후라이와 소시지가 곁들여진 팬케이크",
true,
2.99);
addItem("블루베리 팬케이크",
"신선한 블루베리와 블루베리 시럽으로 만든 팬케이크",
true,
3.49);
addItem("블루베리 팬케이크",
"와플, 취향에 따라 블루베리나 딸기를 얹을 수 있습니다.",
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;
}
}
package com.kws.iterate;
public class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
}
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_ITEMS) {
System.out.println("죄송합니다, 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.");
}
else {
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
}
public MenuItem[] getMenuItems() {
return menuItems;
}
}
- 위의 두개의 메뉴를 사용하는 클라이언트가 있을 경우 어떤 문제점이 생길 수 있을까??
- 클라이언트의 사용하게 될 메서드. ```java printMenu()
- 메뉴에 있는 모든 항목 출력 printBreakfastMenu();
- 아침 항목 출력 printLunchMenu()
- 점심 항목 출력 printVegetarianMenu();
- 채식주의자용 항목 출력 isItemVegetarian(name)
-
채식주의자인지 판단 ```
-
만약 PrintMenu를 구현해야 한다면 PancakeHouseMenu와 DinerMenu의 getMenuItem() 메소드를 호출해서 메뉴 항목을 가져와야 하는데 리턴형식이 하나는 배열이고 하나는 리스트이기 때문에 각각의 순환문으로 돌려서 데이터를 추출해야 한다. 현재 두개의 클래스만 가져와야 하는 상태이기 때문에 두개만 각각 구현하면 되지만 위와 같은 클래스들이 많이 생기게 되고 컬렉션 타입이 다를경우 모두 각각 구현해주어야 한다.
- 이것을 해결 하기 위해서는 각 메뉴에 대한 똑같은 인터페이스를 구현할 수 있게 해주면 된다. 이것을 적용하게 되면 이터레이터 패턴이 된다.
이터레이터 패턴의 적용
public interface Iterator {
boolean hasNext();
Object next();
}
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
@Override
public boolean hasNext() {
if (position >= items.length || items[position] == null ) {
return false;
}
else {
return true;
}
}
@Override
public Object next() {
MenuItem menuItem = items[position];
position++;
return menuItem;
}
}
//DinerMenu클래스
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
public class Waitress {
PancakeHouseMenu pancakeHouseMenu;
DinerMenu dinerMenu;
public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
System.out.println("아침메뉴 \n====");
printMenu(pancakeIterator);
System.out.println("\n점심메뉴 \n====");
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while(iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.print(menuItem.getPrice() + "-- ");
System.out.println(menuItem.getDescription());
}
}
}
public class MenuTest {
public static void main(String [] args) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
DinerMenu dinerMenu = new DinerMenu();
Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
waitress.printMenu();
}
}
- 웨이트리스와 메뉴 분리시키기
//Menu생성
public interface Menu {
public Iterator createIterator();
}
public class Waitress {
Menu pancakeHouseMenu;
Menu dinerMenu;
public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
System.out.println("아침메뉴 \n====");
printMenu(pancakeIterator);
System.out.println("\n점심메뉴 \n====");
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while(iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.print(menuItem.getPrice() + "-- ");
System.out.println(menuItem.getDescription());
}
}
}
단일 역할 원칙
-
집합체에서 내부 컬렉션과 관련된 기능과 반복자용 메소드 관련 기능을 전부 구현하도록 했다면 어떨까??
-
이 방법은 좋은 방법이 아닌데 클래스의 역할 외에 다른 역할을 처리하도록 하면, 두 가지 이유로 인해 그 클래스가 바뀔 수 있다. 이유는 내부컬렉션과 반복자용 메서드 관련 기능이 바뀌면 클래스가 바뀌어야 한다. 이런 이유로 “변경”이라는 주제와 관련된 디자인 원칙이 있다
“클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.”
위코드의 단점
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
- createIterator을 반복호출을 해야한다.
- 그러므로 여러 메뉴를 한꺼번에 관리할 수 있는 방법이 필요하다.
처리방법
public class Waitress {
ArrayList<Menu> menus;
public Waitress(ArrayList<Menu> menus) {
this.menus = menus;
}
public void printMenu() {
Iterator<Menu> menuIterator = menus.iterator();
while (menuIterator.hasNext()) {
Menu menu = menuIterator.next();
printMenu(menu.createIterator());
}
}
private void printMenu(Iterator<MenuItem> iterator) {
while(iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.print(menuItem.getPrice() + "-- ");
System.out.println(menuItem.getDescription());
}
}
}
위처리시 문제점
- 메뉴가 여러개인것은 해결했지만 MenuItem이라는 특정한 형식이 있기 때문에 메뉴에 서브 메뉴를 추가할 수가 없다.
- 이것을 해결 하기 위해서는 새로운 디자인이 필요하다.
- 메뉴, 서브메뉴, 메뉴 항목 등을 모두 집어 넣을 수 있는 트리 형태의 구조가 필요하다
- 각 메뉴에 있는 모든 항목에 대해서 돌아가면서 어떤 작업을 할 수 있는 방법을 제공해야 하며, 그 방법은 적어도 지금 사용중인 반복자 정도로 편리해야만 한다.
- 더 유연한 방법으로 아이템에 대해서 반복작업을 수행할 수 있어야 한다. 예를 들어, 식당 메뉴에 껴있는 디저트 메뉴에 대해서만 반복 작업을 한다거나 디저트 서브메뉴를 포함한, 메뉴 전체에 대해서 반복작업을 하는 것도 가능해야 한다.
컴포지트 패턴의 정의
- 컴포지트 패턴을 이용하면 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있다.
컴포지트 패턴을 적용한 코드
public 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();
}
}
public class Menu extends MenuComponent {
ArrayList<MenuComponent> menuComponents = new ArrayList<>();
String name;
String description;
public Menu(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponent.remove(menuComponent);
}
@Override
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
@Override
public String getName() {
return name;
}
@Override
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 = iterator.next();
menuComponent.print();
}
}
}
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;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public boolean isVegetarian() {
return vegetarian;
}
public double getPrice() {
return price;
}
public void print() {
System.out.print(" " + getName());
if(isVegetarian()) {
System.out.print("(v)");
}
System.out.println(", " + getPrice());
System.out.println(" -- " + getDescription());
}
}
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
}
public class MenuTest {
public static void main(String [] args) {
MenuComponent pancakeHouseMenu = new Menu("팬케이크 하우스 메뉴", "아침 메뉴");
MenuComponent dinerMenu = new Menu("객체마을 식당 메뉴", "점심 메뉴");
MenuComponent cafeMenu = new Menu("카페메뉴", "저녁메뉴");
MenuComponent dessertMenu = new Menu("디저트 메뉴", "디저트를 즐겨 보세요!");
MenuComponent allMenus = new Menu("전체메뉴", "전체메뉴");
allMenus.add(pancakeHouseMenu);
allMenus.add(dinerMenu);
allMenus.add(cafeMenu);
dinerMenu.add(new MenuItem("파스타", "마리나라 소스 스파게티. 효모빵도 드립니다.",
true, 3.89));
dinerMenu.add(new MenuItem("파스타", "마리나라 소스 스파게티. 효모빵도 드립니다.",
true, 3.89));
dinerMenu.add(new MenuItem("파스타", "마리나라 소스 스파게티. 효모빵도 드립니다.",
true, 3.89));
dinerMenu.add(dessertMenu);
dessertMenu.add(new MenuItem("애플파이", "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플파이", true, 1.59));
dessertMenu.add(new MenuItem("애플파이", "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플파이", true, 1.59));
dessertMenu.add(new MenuItem("애플파이", "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플파이", true, 1.59));
dessertMenu.add(new MenuItem("애플파이", "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플파이", true, 1.59));
dessertMenu.add(new MenuItem("애플파이", "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플파이", true, 1.59));
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}
-
위에서는 한 클래스는 한 역할만 해야 한다고 했는데 잘못된 방법이 아닌가?
- 컴포지트 패턴에서는 단일 역할 원칙을 깨면서 대신에 투명성을 확보하기 위한 패턴이라고 할 수 있다.
- Component 인터페이스에 자식들을 관리하기 위한 기능과 잎으로써의 기능을 전부 집어넣음으로써 클라이언트에서 복합 객체와 잎 노드를 똑같은 방식으로 처리할 수 있도록 할 수 있다. 어떤 원소가 복합 객체인지 잎 노드인지가 클라이언트 입장에서는 투명하게 느껴지는 것이다.
- 상황에 따라 원칙을 적절하게 사용해야 한다는 것을 보여주는 대표적인 사례라고 할 수 있다.
이터레이터와 복합 반복자 추가
public class Menu extends MenuComponent {
//그대로
public Iterator<MenuComponent> createIterator() {
return new CompositeIterator(menuComponents.iterator());
}
}
public class MenuItem extends MenuComponent {
//나머지코드 그대로
public Iterator<MenuComponent> createIterator(){
return new NullIterator();
}
}
//핵심 복합 반복자
public class CompositeIterator implements Iterator<MenuComponent>{
Stack<Iterator<MenuComponent>> stack = new Stack<>();
public CompositeIterator(Iterator<MenuComponent> iterator){
stack.push(iterator);
}
@Override
public boolean hasNext() {
if(stack.empty()){
return false;
}
else {
Iterator<MenuComponent> iterator = stack.peek();
if(!iterator.hasNext()) {
stack.pop();
return hasNext();
}
else {
return true;
}
}
}
@Override
public MenuComponent next() {
if(hasNext()) {
Iterator<MenuComponent> iterator = stack.peek();
MenuComponent component = iterator.next();
if (component instanceof Menu) {
stack.push(((Menu) component).createIterator());
}
return component;
}
return null;
}
}
- 널 반복자를 만든이유
-
그냥 null을 리턴할 경우 클라이언트는 null을 체크해야 하지만 아무것도 하지 않은 널 반복자를 만들경우는 동일한 로직으로 처리를 할 수 있기 때문이다.
- 복합객체란
- 마지막 노드가 아닌 자식노드가 여러개 존재할 수 있는 객체이다.
컴포지트 패턴 정리
- 부분-전체 관계를 가지는 컬렉션이 있고, 그 객체들을 모두 똑같은 방식으로 다루고 싶을 때 쓰이는 패턴이다.
- 구성요소들은 개별객체와 복합객체이다.
- 컴포지트 패턴을 적용할 때는 여러가지 장단점을 고려해야 한다. 상황에 따라 투명성과 안정성 사이에서 적절한 평형점을 찾아야 한다.
Comments