헤드퍼스트 디자인 패턴 9장 | 컬렉션 잘 관리하기 - 반복자 패턴과 컴포지트 패턴
(에릭 프리먼 외 4인, 서환수 옮김, 한빛미디어)
객체 저장 방식을 보여주지 않으면서도 클라이언트가 객체에 일일이 접근할 수 있게 해주는 방법을 알아보겠습니다.
- 351p
메뉴 항목 살펴보기
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 class PancakeHouseMenu {
ArrayList<String> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList<String>();
addItem("K&B 팬케이크 세트", 스크램블 에그와 토스트가 곁들여진 팬케이크, true, 2.99);
}
public void addItem(String name, ...) {
menuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItem.add(menuItem);
=> 메뉴 항목을 추가하고 싶으면 필요한 인자를 전달해서 menuItem 객체를 새로 만들고 그 객체를 ArrayList에 추가
}
public ArrayList<String> getMenuItems() {
return menuItems;
}
public Iterator createIterator() {
return new PancakeHouseMenuIterator(menuItems);
}
public String toString() {
return "Pancake House Menu";
}
=> ArrayList에 메뉴 항목을 저장
=> 각 메뉴 항목은 생성자 내에서 ArrayList에 추가
멜의 메뉴
public class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("채식주의자용 BLT", "통밀 위에...", true, 2.99);
...
}
public void addItem(String name, ...){
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_LISTS) {
sout("죄송합니다")
} else {
menuItem[numberOfItems] = menuItem;
numberOfItems = numberOfItems + 1;
} => 항목 개수를 제한해서, 최대 메뉴 항목 개수 초과 여부 확인
}
- 353 ~ 355p
printMenu(), printBreakfastMenu() 등을 하려고 할 때 각각 형식을 가져오고 다른 반복문을 돌려야 하는 문제 발생
- 357p
for (int i = 0; i < breakfastItems.size(); i++) {
MenuItem menuItem = breakfastItems.get(i);;
}
ArrayList의 size()와 get() 메소드를 사용
for (int i = 0; i < lunchItems.lengh; i++) {
MenuItem menuItem = lunchItems[i];
}
배열의 length 필드와 배열 첨자를 사용
이제 객체 컬렉션의 반복 작업 처리 방법을 캡슐화한 Iterator라는 객체를 만들면 어떨까요?
Iterator iterator = breakfastMenu.createIterator();
while (iterator.hasNext()) {
MenuItem menuItem = iterator.next();
}
런치 메뉴도 동일하게 적용 가능
- 359 ~ 360p
Iterator 인터페이스 정의하기
public interface Iterator {
boolean hasNext();
Object next();
=> hasNext() 메소드는 반복 작업을 수행할 항목이 있는지 확인한 다음 그 결과를 불리언 값으로 리턴
=> next() 메소드는 다음 항목을 리턴
}
public class DinerMenuIterator implements Iterator {
String[] items;
int position = 0;
=> 반복작업이 처리되고 있는 위치를 저장
public DinerMenuIterator(String[] items) { => 생성자는 반복 작업을 수행할 메뉴 항목 배열을 인자로 받음
this.items = items;
}
public String next() { => next() 메소드는 배열의 다음 원소를 리턴하고 position 값을 1 증가
String menuItem = items[position];
position = position + 1;
return menuItem;
}
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else {
return true;
}
}
}
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
=> getMenuItems() 메소드는 더 이상 필요 없습니다. 내부 구조를 다 드러내는 단점이 있어서 없애는 게 앗죠.
=> createIterator() 메소드 => menuItems 배열을 가지고 DinerMenuIteraotr를 생성한 다음 클라이언트에게 리턴합니다.
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();
=> 2개의 반복자를 생성합니다. 메뉴마다 하나씩 필요하니까요.
System.out.println("MENU\n----\nBREAKFAST");
printMenu(pancakeIterator);
System.out.println("\nLUNCH");
printMenu(dinerIterator);
=> 반복자를 가지고 오버로드된 printMenu() 메소드를 호출합니다.
}
private void printMenu(Iterator iterator) { => 오버로드된 printMenu() 메소드는 반복자를 써서 모든 메뉴 항목에 접근하여 그 내용을 출력
while (iterator.hasNext()) { 남은 항목이 있는지 확인하고
MenuItem menuItem = iterator.next(); => 다음 항목 가져와서
System.out.print(menuItem.getName() + ", "); => 출력
System.out.print(menuItem.getPrice() + " -- ");
System.out.println(menuItem.getDescription());
}
}
종업원 코드 테스트
public class MenuTestDrive {
public static void main(String args[]) {
Menu pancakeHouseMenu = new PancakeHouseMenu();
Menu dinerMenu = new DinerMenu();
Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
waitress.printMenu();
printMenus();
}
=> 메뉴 생성 = > 두 메뉴를 인자로 전달 = > 종업원 생성 => 메뉴 출력
- 362 ~ 364p
반복자를 사용하면 그 안에 들어 있는 모든 항목에 접근할 수 있게 하려고 여러 메소드를 외부에 노출시키지 않으면서도, 컬렉션에 들어있는 모든 객체에 접근할 수 있습니다. 그리고 반복자를 구현한 코드를 컬렉션 밖으로 끄집어낼 수 있다는 장점도 있죠. 그러니까 반복 작업을 캡슐화했다고 말할 수 있습니다.
- 367p
java.util.Iterator 적용하기
반복자를 직접 만드는 대신 menuItems ArrayList의 iterator() 메소드만 호출하면 됩니다.
public interface Menu {
public Iterator<MenuItem> createIterator();
}
=> 클라이언트에서 메뉴에 들어있는 항목의 반복자를 획득할 수 있게 해주는 간단한 인터페이스
- 369 ~ 370p
ArrayList도 Iterable 이므로 다음과 같이 간편하게 자바의 향상된 for 순환문을 사용할 수 있습니다.
for (MenuItem item : menu) {
///
}
=> hasNext(), next() 메소드를 직접 사용하지 않아도 됩니다.
- 378p
하지만 배열은 iterable 인터페이스를 구현하지 않았기 때문에 향상된 for문 사용 불가
breakfastItems는 forEach 문으로도 출력 가능
- 379p
반복자와 컬렉션 활용하기
- 387p
종업원 코드 개선하기
public void printMenu() {
Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
Iterator<MenuItem> cafeIterator = cafeMenu.createIterator();
System.out.println("MENU\n----\nBREAKFAST");
printMenu(pancakeIterator);
System.out.println("\nLUNCH");
printMenu(dinerIterator);
System.out.println("\nDINNER");
printMenu(cafeIterator);
}
=> createIterator()를 3번 호출합니다.
=> printMenu()도 3번 호출해야 하죠
=> 메뉴를 추가하거나 삭제할 때마다 이 코드를 직접 수정해야 합니다.
public class Waitress {
List<Menu> menus;
public Waitress(List<Menu> menus) {
this.menus = menus;
}
public void printMenu() {
Iterator<Menu> menuIterator = menus.iterator();
while(menuIterator.hasNext()) {
Menu menu = menuIterator.next();
printMenu(menu.createIterator());
}
}
void printMenu(Iterator<MenuItem> iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = iterator.next();
print...
=> 이제 각 메뉴를 따로 받지 않고 목록으로 받아옵니다.
=> 각 메뉴에 반복 작업을 수행합니다.
=> 각 메뉴의 반복자를 오버로드된 printMenu() 메소드에 넘겨주면 되죠.
- 390p
컴포지트 패턴의 정의
컴포지트 패턴으로 객체를 트리구조로 구성해서 부분-전체 계층구조를 구현합니다. 컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.
- 394p
- 402p
컴포지트 적용한 종업원 코드
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
}
=> 다른 모든 메뉴를 포함하고 있는 최상위 메뉴 구성 요소만 넘겨주면 됨
=> 최상위 메뉴 : allMenus
=> 메뉴 전체의 계층구조를 출력하고 싶다면 그냥 최상위 메뉴의 print()메소드만 호출
메뉴 구성 요소 구현하기
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();
}
=> menuItem에서 작업을 처리하는 메소드
}
menuCompent 인터페이스를 확장한 MenuItem
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 class Menu extends MenuComponent {
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);
}
=> MenuItem이나 다른 menu를 추가하는 코드
=> menuItem과 menu 모두 MenuComponent이므로 한 메소드만 가지고 둘 다 처리할 수 있음
...
=> 메뉴에는 MenuComponent 형식의 자식을 몇개든지 저장할 수 있음 = 여기서는 ArrayList에 저장
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();
}
}
= > iterator 반복자를 사용해서 menu에 있는 모든 구성 요소를 대상으로 반복 작업을 처리
=> menu와 menuItem에서 모두 print()를 구현하므로 그냥 print()만 호출하고 나머지는 각 클래스에게 맡김
- 396 ~ 402p
'Book' 카테고리의 다른 글
[독서 기록] 헤드퍼스트 디자인 패턴 11장 | 프록시 패턴 - 객체 접근 제어하기 (0) | 2022.11.15 |
---|---|
[독서 기록] 헤드퍼스트 디자인 패턴 10장 | 상태 패턴 - 객체의 상태 바꾸기 (0) | 2022.11.15 |
[독서 기록] 헤드퍼스트 디자인 패턴 8장 | 알고리즘 캡슐화하기 - 템플릿 메소드 패턴 (0) | 2022.11.15 |
[독서 기록] 헤드퍼스트 디자인 패턴 7장 | 적응시키기 - 어댑터 패턴과 퍼사드 패턴 (0) | 2022.11.15 |
[독서 기록] 헤드퍼스트 디자인 패턴 6장 | 커맨트 패턴 - 호출 캡슐화하기 (0) | 2022.11.15 |