본문 바로가기

IT/헤드퍼스트 디자인패턴

[디자인패턴] 이터레이터 패턴

안녕하세요 남갯입니다.


오늘은 이터레이터 패턴에 대해 포스팅 해보려고 합니다.



사건의 발단

객체마을의 식당과 객체마을의 팬케이크 하우스의 두 가게가 합쳐지면서 메뉴를 합의해서 바꿔야하는 원인이 생겼습니다.

한쪽은 ArrayList를 이용해서 만들었고 한쪽은 배열을 써서 만들었습니다.


두 가게는 메뉴를 구현하는 방법에 대해 합의를 보게 되었습니다.



class MenuItem(
val name: String,
val description: String,
val vegetarian: Boolean,
val price: Double
)


이렇게 말이죠


두 가게가 어떤 문제때문에 싸우는지 보겠습니다.


팬케익을 만드는 하우스의 메뉴는 아래와 같습니다.

class pancakeHouseMenu(val menuItems: ArrayList<MenuItem> = arrayListOf()) {
init {
addItem("펜케이크1", "맛있음1", true, 2.99)
addItem("펜케이크2", "맛있음2", false, 2.99)
addItem("펜케이크3", "맛있음3", true, 3.49)
addItem("펜케이크4", "맛있음4", true, 3.59)
}
fun addItem(
name: String,
description: String,
vegetarian: Boolean,
price: Double
) = menuItems.add(MenuItem(name, description, vegetarian, price))
}


저녁메뉴를 파는곳은 아래와 같습니다. 


class DinerMenu(var menuItems: Array<MenuItem?>) {
val MAXSIZE = 6
var numberofItems: Int = 6

init {
menuItems = arrayOfNulls<MenuItem>(MAXSIZE)
addItem("베이컨 상추메뉴", "맛있음1", true, 2.99)
addItem("통밀", "맛있음2", false, 2.99)
addItem("스프", "맛있음3", false, 3.29)
addItem("핫도그", "맛있음4", false, 3.05)
}

fun addItem(
name: String,
description: String,
vegetarian: Boolean,
price: Double
) {
val menu = MenuItem(name, description, vegetarian, price)
if (numberofItems >= MAXSIZE) {
println("메뉴 꽉 찼습니다.")
} else {
menuItems[numberofItems] = menu
numberofItems ++
}
}
}


이렇게 배열을 통해 구성이 되어 있습니다.


하지만 위에꺼는 ArrayList를 통해 구성했고 아래꺼는 배열을 통해 구성했습니다.


이렇게 다르게 표현해서 구성하면 무슨 문제가 있을까요?


다른사람이 자바 웨이트라는 것을 만들어달라고 주문 했다고 보면


웨이트리스는 주문내역에 맞춰 메뉴를 출력하는 기능, 어떤메뉴항목이 채식주의자인지 알아내는 능력들을 알아야합니다.


fun printMenu() // 메뉴에 있는 모든 항목을 추천
fun printBreakfastMenu() // 아침 식사 항목만 출력
fun printLaunchMenu() // 점심 식사 항목만 출력
fun printVegetarianMenu() //채식주의자용 메뉴 항목만 출력
fun isItemVegetarian() // 채식주의자면 true 아니면 false


웨이트리스의 자격요건을 보면 위에서의 내용과 같이 이런자격요건을 가져야합니다.



각 두개의 다른 메뉴를 호출하려면 각각 getMenuItems를 통해


가져와서 for문을 돌려 다른 메소드를 통해 봐야합니다.


또한 다른가게가 합쳐지면 또 다른 순환문을 합쳐서 세개가 필요합니다.


이런 난처한 상황을 어떻게하면 통합을 할 수 있을까요?


이런 반복을 연속으로 하는것을 캡슐화가 가능할까요?



이터레이터 패턴


우리는 이터레이터 패턴을 통해 해결이 가능합니다


이터래이터 패턴은 반복자 패턴이라고 부릅니다.


이터레이터 패턴에서 가장 중요한점은 Iterator가 인터페이스에 의존합니다 또한 배열 , 리스트, 해시테이블은 물론 객체의 컬렌션에 대해서도 반복자를 구현 가능합니다.



저녁메뉴부분은 이렇게 만들었습니다.

class DinerMenuIterator(val items: Array<MenuItem>) : Iterator<MenuItem> {
var position: Int = 0

override fun hasNext(): Boolean {
return pos()
}

fun pos() = position >= items.size

override fun next(): MenuItem {
val menuItem = items[position]
position++
return menuItem
}
}


이렇게 만든것에다가 dinerMenu에


class DinerMenu(var menuItems: Array<MenuItem?>) {
val MAXSIZE = 6
var numberofItems: Int = 6

init {
menuItems = arrayOfNulls<MenuItem>(MAXSIZE)
addItem("베이컨 상추메뉴", "맛있음1", true, 2.99)
addItem("통밀", "맛있음2", false, 2.99)
addItem("스프", "맛있음3", false, 3.29)
addItem("핫도그", "맛있음4", false, 3.05)
}

fun createIterator(): Iterator<MenuItem> = DinerMenuIterator(menuItems)

fun addItem(
name: String,
description: String,
vegetarian: Boolean,
price: Double
) {
val menu = MenuItem(name, description, vegetarian, price)
if (numberofItems >= MAXSIZE) {
println("메뉴 꽉 찼습니다.")
} else {
menuItems[numberofItems] = menu
numberofItems++
}
}
}



넣고 


class PancakeHouseIterator(val items: ArrayList<MenuItem>) : Iterator<MenuItem> {
var position: Int = 0

override fun hasNext(): Boolean {
return pos()
}

fun pos() = position >= items.size

override fun next(): MenuItem {
val menuItem = items[position]
position++
return menuItem
}
}

pancakeIterator를 위와 같이 만들고


class pancakeHouseMenu(val menuItems: ArrayList<MenuItem> = arrayListOf()) {
init {
addItem("펜케이크1", "맛있음1", true, 2.99)
addItem("펜케이크2", "맛있음2", false, 2.99)
addItem("펜케이크3", "맛있음3", true, 3.49)
addItem("펜케이크4", "맛있음4", true, 3.59)
}

fun createIterator(): Iterator<MenuItem> = PancakeHouseIterator(menuItems)


fun addItem(
name: String,
description: String,
vegetarian: Boolean,
price: Double
) = menuItems.add(MenuItem(name, description, vegetarian, price))
}


createIterator를 만듭니다.


Waitress는


class Waitress(val pancakeHouseMenu: pancakeHouseMenu, val dinerMenu: DinerMenu) {
fun printMenu() {
val pancakeHouseIterator: Iterator<MenuItem> = pancakeHouseMenu.createIterator()
val dinerIterator: Iterator<MenuItem> = dinerMenu.createIterator()
println("메뉴 --- 아침메뉴")
printMenu(pancakeHouseIterator)
println("메뉴 --- 점심메뉴")
printMenu(dinerIterator)
}
fun printMenu(iterator: Iterator<MenuItem>){
while (iterator.hasNext()){
val menuItem = iterator.next()
println(menuItem.name)
println(menuItem.price)
println(menuItem.description)
}
}
}


위에서 만든 Iterator를 통해 메뉴를 프린트하는것을 만듭니다.


기존


1 .기존에 코드에서는 메뉴가 캡슐화가 되어있지 않았고 

ArrayList와 배열을 통해 다른 타입을 썼습니다.

2. 순환문을 두개 필요

3. waitress가 구상클래스와 직접 연결

4. 인터페이스가 똑같은데 다른 구상클래스로 묶여있음


달라진점 

1. 실제 Waitress는 어떤 컬렉션을 어떤식으로 저장하는지 모릅니다.

2. 한개의 순환문으로 처리가능

3. Iterator만 알면 됌

4. 구상클래스가 묶여있음 (아직 인터페이스가 완전 똑같지는 않음)



위에서 달라진점에서 4번 문제중에 인터페이스가 완전 똑같지 않은점이 달랐습니다.



기존의 코드에서


fun createIterator(): Iterator<MenuItem> = PancakeHouseIterator(menuItems)
fun createIterator(): Iterator<MenuItem> = DinerMenuIterator(menuItems)



이렇게 바꿉니다.

fun createIterator(): Iterator<MenuItem> = menuItems.iterator()
fun createIterator(): Iterator<MenuItem?> = menuItems.iterator()



이렇게 변경한뒤에 인터페이스를 통일하고 약간을 고치게 되면 됩니다.


public interface Menu{
fun createIterator() : Iterator<MenuItem>
}


메뉴 관련된 인터페이스를 만들고



class Waitress(val pancakeHouseMenu: Menu, val dinerMenu: Menu) {


이렇게 만들게 되면


특정구현이 아닌 인터페이스에 맞춰서 프로그래밍을 한다라는 원칙을 따르게 되고 의존성을 줄일 수 있습니다.


방금 정의한 Menu 인터페이스에서는 CreateIterator라는 메소드 하나만 있고 이걸통해 PancakeHouseMenu와 DinerMenu를 구현함으로써


구현합니다.



이터레이터 패턴 정의


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