본문 바로가기
JAVA/DesignPattern

이터레이터 패턴 (iterator pattern)

by 램쥐뱅 2016. 6. 30.

이터레이터 패턴 (iterator pattern)


컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있는 방법을 제공한다.



컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식이 통일되어 있으면 어떤 종류의 집합체에 대해서도 사용할 수 있는 다형적인 코드를 만들수 있다.

이터레이터 패턴을 사용하면 모든 항목에 일일이 접근하는 작업을 컬렉션 객체가 아닌 반복자 객체에서 맡게 된다. 이렇게 하면 집합체의 인터페이스 및 구현이 간단해질 뿐 아니라, 집합체에서는 반복작업에서 손을 떼고 원래 자신이 할 일(객체 컬렉션 관리)에만 전념할 수 있다.




이터레이터 패턴 클래스 다이어그램





두개의 서로다른 식당이있고 각각의 식당에서 메뉴를 구현한다고 가정해보자.


public class MenuItem {

String name;

String description;

String vegetarian;

double price;


public MenuItem(String name, String description, boolean vegetarian, double price){

this.nae = 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 class PancakeHouseMenu {

ArrayList<MenuItem> menuItems;


public PancakeHouseMenu() {

this.menuItems = new ArrayList();

additem("K&B 팬케이크 세트","스크램블드 에그와 토스트가 곁들여진 펜케이크",true,2.99);

additem("레귤러 팬케이크 세트","달걀 후라이와 소시지가 곁들여진 펜케이크",false,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);

menuItem.add(menuItem);

}


public ArrayList<MenuItem> getMenuItems() {

return menuItems;

}


//기타 메소드

}



public class DinerMenu {

static final int MAX_ITEMS = 6;

int numberOfItems = 0;

MenuItem[] menuItems;


public DinerMenu() {

this.menuItems = new MenuItem[MAX_ITEMS];

additem("채식주의자용 BLT","통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴",true,2.99);

additem("BLT","통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴",false,2.99);

additem("오늘의 스프","감자 샐러드를 곁들인 오늘의 스프",false,3.29);

additem("핫도그","사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그",false,3.05);

}


public void additem(string name, String description, boolean vegetarian, double price) {

MenuItem menuItem = new MenuItem(name, description, vegetarian, price);

if(nemberOfItems >= MAX_ITEMS){

System.err.println("죄송합니다, 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.");

} else {

menuItems[numberOfItems] = menuItem;

numberOfItems = numberOfItems+1;

}

}


public MenuItem[] getMenuItems() {

return menuItems;

}


//기타 메소드

}



위와 같이 두가지 서로 다은 메뉴 표현 방식이 있을 때 어떤문제가 생길 수 있을까.


두 메뉴를 사용하는 클라이언트를 만들어보자.

클라이언트 기능은 5가지로 정해보자


1. printMenu() - 메뉴에 있는 모든 항목을 출력

2. printBreakfastMenu() - 아침 식사 항목만 출력

3. printLunchMenu() - 점심 식사 항목만 출력

4. printVegetarianMenu() - 채식주의자용 메뉴 항목만 출력

5. isItemVegetarian(name) - name 항목이 채식주의자용이면 true 그렇지 않으면 false를 리턴.



각메뉴에 들어있는 모든항목 출력하려면..


PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();

ArrayList<MenuItem> breakfastItems = pancakeHouseMenu.getMenuItems();


DinerMenu dinerMenu = new DinerMenu();

MenuItem[] lunchItems = dinerMenu.getMenuItems();


for ( int i=0; i < breakfaseItems.size(); i++) {

MenuItem menuItem = breakfastItems.get(i);

System.out.println(menuItem.getName());

System.out.println(menuItem.getPrice());

System.out.println(menuItem.getDescription());

}


for ( int i=0; i < lunchItems.length; i++) {

MenuItem menuItem = lunchItems[i];

System.out.println(menuItem.getName());

System.out.println(menuItem.getPrice());

System.out.println(menuItem.getDescription());

}


다른 모든 메소드들도 결국 위에 있는 코드랑 비슷한 식으로 작성해야 한다.. 항상 두 메뉴를 이용하고, 각아이템에 대해서 반복적인 작업을 수행하기 위해 두 개의 순환문을 써야 한다. 이후에 식당이 더 추가된다면 이런상황이 게속 반복된다.



디자인 원칙중 바뀌는 부분을 캡슐화 하라! 라는 내용이 있었다.

반복을 캡슐화 할 수 있을까??


아래와 같이 반복작업을 캡슐화한 Iterator 라는 객체를 만들면 된다.


Iterator<MenuItem> iterator = breakfastMenu.createIterator();

while(iterator.hasNext()){

MenuItem menuItem = iterator.next();

}


Iterator<MenuItem> iterator = lunchMenu.createIterator();

while(iterator.hasNext()){

MenuItem menuItem = iterator.next();

}



하나의 새로운 Iterator 인터페이스를 만들어도 되지만.

java.util.Iterator 인터페이스를 사용해서 Iterator를 적용시켜보자.



public interface Menu {

public Iterator<MenuItem> createIterator();

|



public class PancakeHouseMenu implements Menu {

ArrayList<MenuItem> menuItems;


public PancakeHouseMenu() {

this.menuItems = new ArrayList();

additem("K&B 팬케이크 세트","스크램블드 에그와 토스트가 곁들여진 펜케이크",true,2.99);

additem("레귤러 팬케이크 세트","달걀 후라이와 소시지가 곁들여진 펜케이크",false,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);

menuItem.add(menuItem);

}


public ArrayList<MenuItem> getMenuItems() {

return menuItems;

}


@Override

public Iterator<MenuItem> createIterator() {

return menuItems.iterator(); //ArrayList 컬렉션은 반복자를 리턴하는 iterator() 라는 메소드가 있음.

}


//기타 메소드

}



public class DinerMenu implements Menu {

static final int MAX_ITEMS = 6;

int numberOfItems = 0;

MenuItem[] menuItems;


public DinerMenu() {

this.menuItems = new MenuItem[MAX_ITEMS];

additem("채식주의자용 BLT","통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴",true,2.99);

additem("BLT","통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴",false,2.99);

additem("오늘의 스프","감자 샐러드를 곁들인 오늘의 스프",false,3.29);

additem("핫도그","사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그",false,3.05);

}


public void additem(string name, String description, boolean vegetarian, double price) {

MenuItem menuItem = new MenuItem(name, description, vegetarian, price);

if(nemberOfItems >= MAX_ITEMS){

System.err.println("죄송합니다, 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.");

} else {

menuItems[numberOfItems] = menuItem;

numberOfItems = numberOfItems+1;

}

}


public MenuItem[] getMenuItems() {

return menuItems;

}


@Override

public Iterator<MenuItem> createIterator() {

return new DinerMenujIterator(menuItems);

}


//기타 메소드

}



public class DinerMenuIterator implements Iterator<MenuItem> {

Menuitem[] list;

int position = 0;


public DinerMenuIterator(MenuItem[] list) {

this.list = list;

}


@Override

public MenuItem next() {

MenuItem menuItem = list[position];

position += 1;

return menuItem;

}


@Override

public boolean hasNext() {

if(position >= list.length || list[position] == null) return false;

else return true;

}


@Override

public void remove() { // 반드시 기능을 제공하지 않아도됨 그렇다면 java.lang.UnsupportedOperationException을 던지도록 하면됨

if(position <= 0) Throw new IllegalStateException("next()가 한번도 호출되지 않음.");

if(list[position-1] != null){

for(int i=position-1; i<(list.length-1); i++){

list[i] = list[i+1];

}

list[list.length-1] = null;

}

}

}



public class Waitress {

ArrayList<Menu> menus;


public Waitress(ArrayList<Menu> menus) {

this.menus = menus;

}


public void printMenu() {

Iterator menuIterator = menus.iterator();

while(menuIterator.hasNext()){

Menu menu = menuIterator.next();

printMenu(menu.createIterator());

}

}


private void printMenu(Iterator<MenuItem> iterator) {

while(iterator.hasNext()) {

MenuItem menuItem = iterator.next();

System.out.println(menuItem.getName());

System.out.println(menuItem.getPrice());

System.out.println(menuItem.getDescription());

}

}

}



public class MenuTestDrive {

public static void main(String args[]) {

ArrayList<Menu> menuList = new ArrayList();

menuList.add(new PancakeHouseMenu());

menuList.add(new DinerMenu());


Waitress waitress = new Waitress(menuList);

waitress.printMenu();

}

}


이제 집합체 내에서 어떤 식으로 일이 처리되는 지에 대해서 전혀 모르는 상태에서도 그 안에 들어있는 모든 항목들에 대해서 반복작업을 수행할수 있게 되었다.



집합체에서 내부 컬랙션과 관련된 기능과 반복자용 메소드 관련기능을 전부 구현하도록 했다면 어떨까?

그렇게 하면 집합체에 들어가는 메소드 개수가 늘어나지.. 그게 나쁜건가?


우선 클래스에서 원래 그 클래스의 역할(집합체 관리) 외에 다른 역할(반복자 메소드)을 처리하 도록 하면, 두 가지 이유로 인해 그 클래스가 바뀔 수 있게 된다.

하나는 컬렉션이 어떤 이유로 인해 바뀌게 되면 그 클래스가 바뀌어야 하고, 반복자 관련 기능이 바뀌었을 때도 클래스가 바뀌여야 된다.


이런 이유로 인해 "변경" 이라는 주제와 관련된 디자인 원칙이 있다.



디자인 원칙


클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.



클래스를 고치는 것은 최대한 피해야 한다. 코드를 변경하다 보면 온갖 문제가 생겨날수 있으니까..

때문에 코드를 변경할 만한 이유가 두가지가 되면 그만큼 그 클래스를 나중에 고쳐야 할 가능성이 커지게 될 뿐 아니라, 디자인에 있어서 두 가지 부분이 동시에 영향이 미치게 된다.

이 원칙에 따르면 한 역할은 한 클래스에서만 맡게 해야 한다.













참고.


Head First Design Patterns.












댓글