👀 학습 목표
- 이터레이터 패턴을 이해하며, 사용하는 목적을 이해한다.
- 예제로 이터레이터 패턴을 이해한다.
1. 이터레이터 패턴이란?
컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목을 접근할 수 있게 해준다.
- 아래와 같이 클라이언트는 타입 상관 없이 두 인터페이스로만 접근하면 되므로 구체적인 타입을 알 필요가 없다.
https://refactoring.guru/design-patterns/iterator 이미지 참고
사용 목적
- 클라이언트한테 객체들의 저장 방식을 모두 알려줘야할까요? 아닙니다. 클라이언트는 저장 방식을 알아서 접근하는 것은 불편합니다. 따라서 이터레이터 패턴을 사용해서 어떤 저장 방식이든 같은 방식으로 접근하게 하는 것이 좋습니다.
2. 이터레이터 패턴 적용
2-1. 객체 마을 식당과 객체 마을 팬케이크 하우스 합병
메뉴 데이터를 저장하는 방식이 다른 식당 2개를 합병하려고한다.
- MenuItem
- 우선 해당 클래스로 메뉴를 관리하기로 결정을 하였다.
// MenuItem: 점심 식사 메뉴 항목을 담는 클래스
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 double getPrice() {
return price;
}
public boolean isVegetarian() {
return vegetarian;
}
public String toString() {
return (name + ", $" + price + "\n " + description);
}
}
- DinerMenu: 객체 식당 메뉴 정의 클래스
// DinerMenu: 객체지향 식당 저녁 메뉴 정의 클래스 - ArrayList가 아닌 배열 사용
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 saurkraut, 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;
}
// other menu methods here
}
- PancakeHouseMenu
- 팬케이크 마을 메뉴 정의 클래스
import java.util.ArrayList;
// PancakeHouseMenu: 팬케이크 마을 메뉴 항목 - 쉽게 추가하기 위해 ArrayList 형식 사용
public class PancakeHouseMenu{
ArrayList menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
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",
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 getMenuItems() {
return menuItems;
}
// other menu methods here
- 이제 이 두 메뉴를 합쳐 보여줄 웨이터인 클라이언트를 구현하자. 구현 기능은 아래와 같다
- printMenu(): 메뉴에 있는 모든 항목 출력
- printBreakfastMenu(): 아침 식사 항목만 출력
- printLunchMenu(): 점심 식사 항목 출력
- printVegerarianMenu(): 채식주의자용 메뉴 항목만 출력
- isItemVegetarian(name): 채식주의자용 인지 확인
- 우선 printMenu 메소드만 구현하자면, 아래와 같다.
- 문제점: 웨이터를 구현하는 것은 불편하고, 코드 관리 확장 등을 하기 힘들어 진다.
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList 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());
}
2-2. 문제를 해결하기 위해 반복을 캡슐화?
지금까지 원칙 중에 “바뀌는 부분을 캡슐화하라” 가 있었다. 이를 참고해서 반복문을 캡슐화해보자.
웨이터 printMenu 바뀌는 부분 찾기
- breakfaseItems.size() / lunchItems.length
- breakfastItems.get(i) / lunchItems[i]
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 인터페이스를 만들어 타입별로 다른 부분을 캡슐화 하자.
- 아래는 다른 식당 2개가 타입이 다른 부분인 next(), hasNext() 메소드를 구현하였다.
코드
- Iterator 인터페이스 정의
// Iterator: Iterator 인터페이스
public interface Iterator {
boolean hasNext(); // 다음 반복 작업을 할 것이 있는지 리턴
Object next(); // 다음 항목 리턴
}
- DinerMenuIterator 클래스 정의, 이터레이터 인터페이스 구현
- 리스트 타입
// DinerMenuIterator: list 형태로 Iterator 정의
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;
// 1. 음식메뉴들을 초기화
public DinerMenuIterator(MenuItem[] items)
{
this.items = items;
}
// 2. next(): 다음 음식 메뉴를 리턴
public MenuItem next() {
MenuItem menuItem = items[position];
position = position + 1;
return menuItem;
}
// 3. hasNext(): 다음 메뉴가 존재하는지 boolean 리턴
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else {
return true;
}
}
}
- PancakeHouseMenuIterator 클래스 정의
- ArrayList 타입 구조 정의
// PancakeHouseMenuIterator: 팬케이크 ArrayList 구조 형태로 iterator 정의
public class PancakeHouseMenuIterator implements Iterator {
ArrayList<MenuItem> items;
int position = 0;
public PancakeHouseMenuIterator(ArrayList<MenuItem> items) {
this.items = items;
}
public MenuItem next() {
MenuItem item = items.get(position);
position = position + 1;
return item;
}
public boolean hasNext() {
if (position >= items.size()) {
return false;
} else {
return true;
}
}
}
모든 메뉴 클래스가 이터레이터를 생성할 수 있는 메소드를 만들자.
- PancakeHouseMenu
public Iterator createIterator() {
return new PancakeHouseMenuIterator(menuItems);
}
- DinerMenu
public Iterator createIterator() { // DinerMenuIterator 생성자 리턴
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() {
// createIterator 메소드 호출하여 이터레이터 생성
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());
}
}
2-3. java.util.literator 적용하기
- PancakeHouseMenu 이터레이터 생성 메소드 수정
- ArrayList 타입인 팬케이크는 이터레이터를 직접 구현할 필요가 없다.
- 이전에 만든 PancakeHouseMenuIterator는 이제 필요없다. 제거하자.
public Iterator createIterator() {
return menuItems.iterator();
}
- DinerMenuIterator 에 remove() 메소드 직접 만들기
- 리스트 타입은 Iterator를 직접 구현해야한다.
import java.util.Iterator;
// DinerMenuIterator: list 형태로 Iterator 정의
public class DinerMenuIterator implements Iterator {
MenuItem[] list;
int position = 0;
// 1. 음식메뉴들을 초기화
public DinerMenuIterator(MenuItem[] items)
{
this.list = items;
}
// 2. next(): 다음 음식 메뉴를 리턴
public MenuItem next() {
MenuItem menuItem = list[position];
position = position + 1;
return menuItem;
}
// 3. hasNext(): 다음 메뉴가 존재하는지 boolean 리턴
public boolean hasNext() {
if (position >= list.length || list[position] == null) {
return false;
} else {
return true;
}
}
// 4. remove()
public void remove() {
if (position <= 0) {
throw new IllegalStateException
("You can't remove an item until you've done at least one 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;
}
}
}
}
2-4. 메뉴 인터페이스 통일 시키기
- Menu 인터페이스 만들기
- 두 가지 타입인 메뉴에 “implements Menu” 추가하기
public interface Menu {
public Iterator createIterator();
}
- 웨이터 변경
- Menu 부분 변경
import java.util.Iterator;
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("MENU\n----\nBREAKFAST");
printMenu(pancakeIterator);
System.out.println("\nLUNCH");
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());
}
}
참고
- Head first design patterns 책