[헤드퍼스트 디자인패턴] 9-1. 반복자 패턴(Iterator Pattern)
반복자 패턴(Iterator Pattern)
반복 작업을 Iterator 인터페이스로 캡슐화해 데이터가 저장될 컬렉션을 외부로 노출시키지 않으면서 컬렉션 내 모든 항목에 접근할 수 있는 패턴입니다.
- 어떤 식으로 구현되어 있는지 전혀 모르는 상태에서 반복 작업을 수행할 수 있습니다.
- 배열이든, 리스트이든 컬렉션의 종류와 상관없이 작업을 처리할 수 있습니다.
- 모든 항목에 일일이 접근하는 작업을 컬렉션이 아닌 반복자 객체가 담당합니다.
구조
1. Iterator 인터페이스: 모든 반복자가 구현해야 하는 인터페이스로 컬렉션을 순회하는데 필요한 작업들을 제공합니다.
2. ConcreateIterator 구상 클래스: 컬렉션을 순회하기 위한 알고리즘을 구현합니다.
3. IterableCollection 인터페이스: 반복자로 관리할 컬렉션들의 공통 인터페이스를 정의합니다.
4. ConcreateCollection 구상 클래스: 클라이언트가 사용할 객체 컬렉션을 갖고 있으며, Iterator로 리턴하는 메소드를 구현해야 합니다.
5. Client: 인터페이스를 통해 반복자와 컬렉션을 사용합니다.
단일 책임 원칙(SRP)
객체 지향 설계 5원칙 중 하나로, 다음과 같은 의미를 가지고 있습니다.
어떤 클래스가 바뀌는 이유는 단 하나뿐이어야 한다.
어떤 클래스가 맡고 있는 모든 역할은 나중에 코드의 변화를 불러올 수 있습니다.
즉 2개 이상의 역할을 맡으면 바뀔 수 있는 부분이 2개 이상이 될 수 있습니다.
이렇듯 한 클래스가 특정 역할을 얼마나 일관되게 지원하는지를 측정하는 척도로 '응집도'를 사용합니다.
응집도가 높을 수록 서로 연관된 기능이 묶여있음을 의미합니다.
반복자 패턴은 컬렉션으로부터 Iterator 클래스로 반복자용 메소드를 분리하기 때문에 SRP 원칙을 잘 준수하고 있는 예시로 볼 수 있습니다.
예시
식당과 팬케이크 가게에서 사용하는 서로 다른 메뉴판을 하나의 메뉴판으로 합쳐봅시다!
팬케이크 가게의 메뉴들
public class PancakeHouseMenu {
List<MenuItem> menuItems;
public PancakeHouseMenu() {
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, vegeterian, price);
menuItems.add(menuItem);
}
public ArrayList<MenuItem> getMenuItems() {
return menuItems;
}
}
식당의 메뉴들
public class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public PancakeHouseMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("채식주의자용 BLT", "통밀, 콩고기 베이컨, 상추, 토마토", true, 2.99);
addItem("BLT", "통밀, 베이컨, 상추, 토마토", 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, vegeterian, price);
if (numberOfItems >= MAX_ITEMS) {
System.err.println("메뉴가 꽉 차 더 이상 추가할 수 없습니다.");
} else {
menuItems[numberOfItems ] = menuItem;
numberOfItems += 1;
}
}
public MenuItem[] getMenuItems() {
return menuItems;
}
}
두 가게가 서로 다른 컬렉션을 사용하고 있습니다. 두 컬렉션을 Iterator 인터페이스로 반환하는 메소드를 추가해줍시다.
PancakeHouseMenu에 메소드 추가
public Iterator<MenuItem> createIterator() {
return menuItems.iterator(); // Java 컬렉션에서 제공하는 기본 Iterator
}
DinerMenu에 메소드 추가
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
DinerMenuIterator 클래스 생성하기
public class DinerMenuIterator implements Iterator<MenuItem> {
MenuItem[] items;
int position = 0;
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
public MenuItem next() {
MenuItem menuItem = items[position];
position += 1;
return menuItem;
}
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else {
return true;
}
}
public void remove() {
throw new UnsupportedOperationException("메뉴 항목을 지울 수 없습니다.");
}
}
이제 종업원은 Iterator 인터페이스를 통해 두 가게의 메뉴판을 하나로 통합하여 사용할 수 있습니다.
public class Waitress {
Menu pancakeHouseMenu;
Menu dinerMenu;
public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
System.out.println("MENU");
printMenu(pancakeIterator);
printMenu(dinerIterator);
}
private void printMenu(Iterator iter) {
while (iter.hasNext()) {
MenuItem menuItem = iter.next();
System.out.print(menuItem.getName() + ", ");
...
}
}
}