전체 페이지뷰

2017년 3월 17일 금요일

Iterator Pattern

배열, 스택, 해시테이블, 리스트 등 여러가지 자료들의 집합인 컬렉션이 있습니다. 이 컬렉션의 내용을 보고 싶을 때, 객체를 저장하는 방식은 보여주지 않으면서도 클라이언트가 객체들에게 일일이 접근할 수 있게 해주는 패턴이 바로 이터레이터 입니다.

객체마을 식당과 팬케이크 하우스의 합병을 가지고서 Iterator의 설명을 진행합니다.
아침 메뉴에는 팬케이크하우스 메뉴를 사용하고 점심에는 객체마을 식당의 메뉴를 사용하기로 하고, 일단 메뉴 항목을 구현하는 방법은 정해졌지만, 세부적으로는 코드가 많이 달라 어떤 식으로 구현할지에 대한 결정이 되지 않았습니다.

MenuItem


먼저 합의된 메뉴 항목인 MenuItem에 대해 살펴보도록 하겠습니다.
public class MenuItem
{
    string name;
    string description;
    bool vegetarian;
    double price;
    public MenuItem(string name, 
        string description,
        bool 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 bool IsVegetarian()
    {
        return vegetarian;
    }
}
cs

메뉴 내부에는 이름, 설명, 채식 여부, 가격 등의 필드가 정의되어 있습니다.
이제 각각의 가게에서 기존에 쓰였던 구현법을 살펴보고 어떤 것들이 서로 다른가를 알아보도록 합시다.

서로 다른 메뉴 구현법

팬케이크 하우스

public class PancakeHouseMenu
{
    List<MenuItem> menuItems;
    public PancakeHouseMenu()
    {
        menuItems = new List<MenuItem>();
        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, 
        bool vegetarian, 
        double price)
    {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        menuItems.Add(menuItem);
    }
    public List<MenuItem> GetMenuItems()
    {
        return menuItems;
    }
// 기타
}
cs

새로운 항목 추가를 용이하게 하기 위해 List<MenuItem>을 사용했습니다. 생성자 내부에서 메뉴항목을 직접 추가하게 되어 있고, GetMenuItems()를 통해 메뉴 항목을 리턴합니다.

객체마을 식당

public class DinnerMenu
{
    const int MAX_ITEMS = 6;
    int numberOfItems = 0;
    MenuItem[] menuItems;
    public DinnerMenu()
    {
        menuItems = new MenuItem[MAX_ITEMS];
        AddItem("채식주의자용 BLT",
            "통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴",
            true2.99);
        AddItem("BLT",
            "통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴"false2.99);
        AddItem("오늘의 스프",
            "감자 샐러드를 곁들인 오늘의 스프"false3.29);
        AddItem("핫도그",
            "사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그"false3.05);
        // 기타 메뉴 항목이 추가되는 부분
    }
     public void AddItem(string name, 
        string description, 
        bool vegetarian, 
        double price)
    {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        if (numberOfItems >= MAX_ITEMS)
        {
            Console.WriteLine("메뉴가 다 찼습니다. 더 이상 추가할 수 없습니다.");
        }
        else
        {
            menuItems[numberOfItems] = menuItem;
            numberOfItems += 1;
        }
    }
    public MenuItem[] GetMenuItems()
    {
        return menuItems;
    }

// 기타 코드
}
cs


상수로 최대 메뉴 수를 정해 두었습니다. 그리고 배열을 사용하여 메뉴를 생성하고 추가합니다. AddItem() 내에서 정해진 메뉴 수를 넘지 못하도록 하는 장치를 마련해 두었습니다.

메뉴 구현 방식이 다를 때의 문제점


이 두 가지 메뉴를 이용하는 클라이언트인 웨이트리스 코드를 만들어 보도록 합니다. 이 웨이트리스는 손님이 주문한 내용을 출력하는 기능과 어떤 메뉴가 채식주의자용인지 알아낼수 있어야 합니다.

구현할 메소드
  • PrintMenu() : 메뉴상의 모든 항목을 출력
  • PrintBreakfastMenu() : 아침 식사 항목 출력
  • PrintLunchMenu() : 점심 식사 항목 출력
  • PrintVegetarianMenu() : 채식주의자용 메뉴 출력
  • IsItemVegetarian(name) :  채식주의자용이면 true, 아니면 false 리턴

PrintMenu()부터 구현할 방법을 생각해 봅시다.
① PancakeHouseMenuDinnerMenuGetMenuItems() 메소드를 호출하여 항목을 가져와야 하는데 둘의 리턴 형식이 다릅니다.

PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
List<MenuItem> breakfastItems= pancakeHouseMenu.GetMenuItems();

DinnerMenu dinnerMenu = new DinnerMenu();
MenuItem[] lunchItems = dinnerMenu.GetMenuItems();

PancakeHouseMenuDinnerMenu에 있는 항목에 있는 것들을 출력하기 위해서는 각각 List와 배열에 대해 순환문을 돌려야 합니다.
foreach(MenuItem menuItem in breakfastItems)
{
    Console.Write(menuItem.GetName() + " ");
    Console.WriteLine(menuItem.GetPrice());
    Console.WriteLine(menuItem.GetDescription());
}
for(int i = 0; i < lunchItems.Length; i++)
{
    MenuItem menuItem = lunchItems[i];
    Console.Write(menuItem.GetName() + " ");
    Console.WriteLine(menuItem.GetPrice());
    Console.WriteLine(menuItem.GetDescription());
}
cs

* 배열을 6개로 선언했는데 4개밖에 내용이 들어있지 않아 이 코드를 그냥 돌리면 NullReferenceException이 납니다.

③다른 메소드들도 이런 식으로 두가지 메뉴를 따로 생성하고 불러와 따로 처리하는 방법을 사용해야만 하는 어려움이 있습니다.

두 메뉴의 구현 방식이 다르기 때문에 사용하기가 번거로울 뿐 아니라 추후 관리와 확장에도 어려움이 있습니다. 이 두 메뉴에 대해 같은 인터페이스를 구현할 수 있다면 상당히 편리해질 것입니다. 그렇다면 어떻게 해야 할까요?


반복의 캡슐화?

"바뀌는 부분을 캡슐화하라"는 내용은 이미 보셨습니다. 지금 두 메뉴의 컬렉션 형식이 달라 반복작업을 하는 부분을 캡슐화하려면 어떻게 해야 할까요?
여기서 필요한 것이 바로 이터레이터(iterator, 반복자) 패턴입니다.

이터레이터 패턴의 인터페이스는 그림과 같습니다.

HasNext() 메소드로 반복작업을 적용할 대상이 더 있는지 확인 가능하고, Next()는 다음 객체를 리턴합니다. 이 인터페이스를 사용하면 어떤 종류의 컬렉션에 대해서도 반복자를 구현할 수 있습니다.

이제 이 Iterator와 그것을 DinnerMenu에 적용한 DinnerMenuIterator를 만들어 보도록 합니다.

먼저 인터페이스를 정의합니다.
public interface Iterator
{
    bool HasNext();
    object Next();
}
cs

그리고 구상 Iterator 클래스인 DinnerMenuIterator를 작성합니다.
public class DinnerMenuIterator : Iterator
{
    MenuItem[] items;
    int position = 0;
    public DinnerMenuIterator(MenuItem[] items)
    {
        this.items = items;
    }
    public object Next()
    {
        MenuItem menuItem = items[position];
        position += 1;
        return menuItem;
    }
    public bool HasNext()
    {
                if (position>=items.Length || items[position] == null)
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}
cs

position으로 반복 작업시 필요한 현재 위치를 저장합니다. 그라고, 생성자에서는 반복작업을 수행할 메뉴 배열을 받아옵니다.
Next() 에서는 배열의 다음 원소를 리턴하고 position 변수를 1 증가 시켜서 현재 위치를 변화시킵니다.
HasNext()에서는 배열의 모든 항목을 돌았는지 확인하고 아직 남은 원소가 있다면 true를 반환합니다. 덧붙여 여기서는 메뉴의 최대 개수에 못 미치는 수의 메뉴가 들어있는 경우를 대비하여 배열의 항목이 null인지도 체크합니다.

DinnerMenu도 수정이 필요합니다. 이제 반복자 패턴이 있으므로 GetMenuItems()는 필요없고 단순히 DinnerMenuIterator를 생성하여 리턴해주는 CreateIterator()를 작성합니다.
/*
public MenuItem[] GetMenuItems()
{
    return menuItems;
}
*/
public Iterator CreateIterator()
{
    return new DinnerMenuIterator(menuItems);
}
cs
GetMunuItems()는 지우고 CreateIterator()를 작성했습니다.

PancakeHouseIterator도 같은 방법으로 구현합니다.

다음으로 할 일은 웨이트리스 코드를 수정하는 것입니다. 반복작업이 좀 더 단순해집니다.
public class Waitress
{
    PancakeHouseMenu pancakeHouseMenu;
    DinnerMenu dinnerMenu;
    public Waitress(PancakeHouseMenu pancakeHouseMenu, DinnerMenu dinnerMenu)
    {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinnerMenu = dinnerMenu;
    }
    public void PrintMenu()
    {
        Iterator pancakeIterator = pancakeHouseMenu.CreateIterator();
        Iterator dinnerIterator = dinnerMenu.CreateIterator();
        Console.WriteLine("메뉴\n----\n아침 메뉴");
        PrintMenu(pancakeIterator);
        Console.WriteLine("\n점심 메뉴");
        PrintMenu(dinnerIterator);
    }
    private void PrintMenu(Iterator iterator)
    {
        while (iterator.HasNext())
        {
            MenuItem menuItem = (MenuItem)iterator.Next();
            Console.Write(menuItem.GetName() + ", ");
            Console.Write(menuItem.GetPrice() + " -- ");
            Console.WriteLine(menuItem.GetDescription());
        }
    }
    // 기타 메소드
}
cs

생성자에서 두 메뉴를 인자로 받아옵니다. PrintMenu() 내에서 두 개의 반복자를 생성하고, 오버로드된 PrintMenu()메소드를 호출합니다.
오버로드된 PrinMenu()에서는 반복자를 사용하여 모든 메뉴 항목에 접근합니다.
이제 순환문을 하나만 사용할 수 있게 되었습니다.

테스트를 작성하여 잘 돌아가는지 봅시다.
static void Main(string[] args)
{
    PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
    DinnerMenu dinnerMenu = new DinnerMenu();
    Waitress waitress = new Waitress(pancakeHouseMenu, dinnerMenu);
    waitress.PrintMenu();
}
cs

결과)
메뉴
----
아침 메뉴
K&B 팬케이크 세트, 2.99 -- 스크램블드 에그와 토스트가 곁들여진 팬케이크
레귤러 팬케이크 세트, 2.99 -- 달걀 프라이와 소시지가 곁들여진 팬케이크
블루베리 팬케이크, 3.49 -- 신선한 블루베리와 블루베리 시럽으로 만든 팬케이크
와플, 3.59 -- 와플, 취향에 따라 블루베리나 딸기를 얹을 수 있습니다.

점심 메뉴
채식주의자용 BLT, 2.99 -- 통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴
BLT, 2.99 -- 통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴
오늘의 스프, 3.29 -- 감자 샐러드를 곁들인 오늘의 스프
핫도그, 3.05 -- 사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그

이터레이터 패턴을 사용함으로써 메뉴가 캡슐화되었고, 순환문을 통합하여 하나만 쓸 수 있게 되었습니다. 또한 각각의 메누 구현방법을 알 필요 없이 이터레이터 인터페이스만 알고 있으면 됩니다.

그러나 아직 문제 해결이 끝나지 않았습니다. 두 개의 메뉴가 똑같은 메소드들을 제공하는데도 인터페이스를 통합하지 않아 여전히 두개의 구상 클래스에 묶여 있다는 점입니다.
"특정 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다"는 원칙을 지키기 위해 메뉴에 대한 인터페이스를 추가합니다.

public interface Menu
{
    Iterator CreateIterator();
}
cs

그리고, PancakeHouseMenu, DinnerMenu 클래스가 이를 상속하도록 : Menu 를 추가하고,
Waitress 클래스에서
public class Waitress
{
    PancakeHouseMenu pancakeHouseMenu;
    DinnerMenu dinnerMenu;
 
    public Waitress(PancakeHouseMenu pancakeHouseMenu, DinnerMenu dinnerMenu)
    {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinnerMenu = dinnerMenu;
    }
    // 이하 생략
}
cs

를 이렇게 고쳐서 메뉴가 하나의 인터페이스로 통합되도록 합니다.It
public class Waitress
{
    Menu pancakeHouseMenu;
    Menu dinnerMenu;
 
    public Waitress(Menu pancakeHouseMenu, Menu dinnerMenu)
    {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinnerMenu = dinnerMenu;
    }
    // 이하 생략
}
cs


Iterator 패턴의 정의



  • 컬렉션 구현 방법은 노출시키지 않고 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해주는 방법을 제공해 줌

사실 C#에서 이터레이터 패턴을 구현하는 것은 IEnumerable, IEnumerator 인터페이스를 이용하여 구현할 수 있습니다. (본인 글 참고) 하지만 여기서는 이터레이터가 어떤 것인지 알아보는 것이므로 직접 구현하는 방법을 사용하는 것이 맞을 것 같습니다.

마지막으로 이터레이터 패턴의 UML을 살펴보고 지금까지 사용한 코드들을 올리고 마치도록 하겠습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
 
 
namespace IteratorPattern
{
    class Program
    {
        // 이터레이터 인터페이스
        public interface Iterator
        {
            bool HasNext();
            object Next();
        }
 
        // 메뉴 인터페이스
        public interface Menu
        {
            Iterator CreateIterator();
        }
        // 공통 메뉴 코드
        public class MenuItem 
        {
            string name;
            string description;
            bool vegetarian;
            double price;
 
            public MenuItem(string name, 
                string description,
                bool 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 bool IsVegetarian()
            {
                return vegetarian;
            }
        }
 
        // 각 식당 메뉴
        public class PancakeHouseMenu : Menu
        {
            List<MenuItem> menuItems;
 
            public PancakeHouseMenu()
            {
                menuItems = new List<MenuItem>();
 
                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, 
                bool vegetarian, 
                double price)
            {
                MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
                menuItems.Add(menuItem);
            }
 
            /*
            public List<MenuItem> GetMenuItems()
            {
                return menuItems;
            }
            */
 
            public Iterator CreateIterator()
            {
                return new PancakeHouseIterator(menuItems);
            }
        }
 
        public class DinnerMenu : Menu
        {
            const int MAX_ITEMS = 6;
            int numberOfItems = 0;
            MenuItem[] menuItems;
 
            public DinnerMenu()
            {
                menuItems = new MenuItem[MAX_ITEMS];
 
                AddItem("채식주의자용 BLT",
                    "통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴",
                    true2.99);
                AddItem("BLT",
                    "통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴"false2.99);
                AddItem("오늘의 스프",
                    "감자 샐러드를 곁들인 오늘의 스프"false3.29);
                AddItem("핫도그",
                    "사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그"false3.05);
                // 기타 메뉴 항목이 추가되는 부분
 
            }
 
            public void AddItem(string name, 
                string description, 
                bool vegetarian, 
                double price)
            {
                MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
                if (numberOfItems >= MAX_ITEMS)
                {
                    Console.WriteLine("메뉴가 다 찼습니다. 더 이상 추가할 수 없습니다.");
                }
                else
                {
                    menuItems[numberOfItems] = menuItem;
                    numberOfItems += 1;
                }
            }
 
            /*
            public MenuItem[] GetMenuItems()
            {
                return menuItems;
            }
            */
 
            public Iterator CreateIterator()
            {
                return new DinnerMenuIterator(menuItems);
            }
        }
 
        // 구상 이터레이터 클래스
        public class DinnerMenuIterator : Iterator
        {
            MenuItem[] items;
            int position = 0;
 
            public DinnerMenuIterator(MenuItem[] items)
            {
                this.items = items;
            }
 
            public object Next()
            {
                MenuItem menuItem = items[position];
                position += 1;
                return menuItem;
            }
 
            public bool HasNext()
            {
                if (position>=items.Length || items[position] == null)
                {
                    return false;
                }
                else
                {
                    return true;
                }
            }
        }
 
        public class PancakeHouseIterator : Iterator
        {
            List<MenuItem> items;
            int position = 0;
 
            public PancakeHouseIterator(List<MenuItem> items)
            {
                this.items = items;
            }
            public object Next()
            {
                MenuItem menuItem = items[position];
                position += 1;
                return menuItem;
            }
            public bool HasNext()
            {
                if (position >= items.Count)
                {
                    return false;
                }
                else
                {
                    return true;
                }
            }
        }
 
        // 웨이트리스 코드
        public class Waitress
        {
            Menu pancakeHouseMenu;
            Menu dinnerMenu;
 
            public Waitress(Menu pancakeHouseMenu, Menu dinnerMenu)
            {
                this.pancakeHouseMenu = pancakeHouseMenu;
                this.dinnerMenu = dinnerMenu;
            }
 
            public void PrintMenu()
            {
                Iterator pancakeIterator = pancakeHouseMenu.CreateIterator();
                Iterator dinnerIterator = dinnerMenu.CreateIterator();
                Console.WriteLine("메뉴\n----\n아침 메뉴");
                PrintMenu(pancakeIterator);
                Console.WriteLine("\n점심 메뉴");
                PrintMenu(dinnerIterator);
            }
 
            private void PrintMenu(Iterator iterator)
            {
                while (iterator.HasNext())
                {
                    MenuItem menuItem = (MenuItem)iterator.Next();
                    Console.Write(menuItem.GetName() + ", ");
                    Console.Write(menuItem.GetPrice() + " -- ");
                    Console.WriteLine(menuItem.GetDescription());
                }
            }
 
            // 기타 메소드
        }
        static void Main(string[] args)
        {
            PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
            DinnerMenu dinnerMenu = new DinnerMenu();
 
            Waitress waitress = new Waitress(pancakeHouseMenu, dinnerMenu);
            waitress.PrintMenu();
        }
    }
}
 
cs

댓글 없음:

댓글 쓰기