전체 페이지뷰

2017년 3월 29일 수요일

Composite Pattern, part 1


이 번 글은 아직 컴포지트 패턴으로 들어가지 않았으며 여전히 이터레이터 패턴에 관한 것입니다. 다음으로 나가기 위한 준비 단계라고 보아 주시면 될 것 같습니다.


앞에서 설명된 이터레이터 패턴에서 이어지는 내용입니다. 이터레이터 패턴의 예에서 객체마을 식당과 팬케이크 하우스의 메뉴를 합치는 과정을 살펴봤는데, 이제 또 다시 카페가 하나더 합병됩니다.

카페에서 사용되던 메뉴를 한번 살펴봅시다.(사실 책에서 지난번 ArrayList로 되어 있던 팬케이크하우스의 메뉴를 제 마음대로 List<T>로 변경했었는데 이번에는 Hashtable이 등장하는군요. 이건 그냥 C#에서도 Hashtable로 가보기로 하겠습니다.

IEnumerator의 사용

여기서부터 책에서는 기존의 코드를 수정하여 직접 Iterator 인터페이스를 만들어서 구현하는 대신, Java의 유틸인 java.util.Iterator를 이용해 간단하게 Iterator를 만드는 법을 설명했습니다. 따라서 제 고민도 깊어졌습니다. 어차피 패턴을 배우기 위한 것인데 그냥 기존의 코드대로 갈 것인지, 아니면 IEnumerator를 사용해서 코드를 전면 재수정할 것인지. 문제는 제가 아직 미숙하여 IEnumerator에 대해 잘 모른다는 것이었습니다(독학은 괴롭습니다 ㅎㅎ). 이렇게도 해보고 저렇게도 시도해 보다가(간단히 얘기했지만 삼일간 고민했습니다 ㅠㅠ) 코드를 전면 재수정하여 IEnumeratorGetEnumerator()메소드를 사용하여 Iterator 객체를 리턴하는 방법을 취하기로 하고 미숙하게나마 수정해 봤습니다.

먼저 IEnumerator에 대해서 간략히 알아봅시다.
msdn 설명으로는 "제너릭이 아닌 컬렉션을 단순하게 반복할 수 있도록 지원한다"고 되어 있습니다.

속성으로는
Current : 컬렉션의 현재 요소를 가져온다

메소드로
MoveNext() : 열거자를 다음 요소로 이동하고, 이동한 경우 true, 끝난 경우 false를 반환
Reset() : 컬렉션의 첫번째 요소로 이동

이 있습니다.

이것을 이용하기 위해 먼저 스스로 만든 Iterator 인터페이스를 지웁니다. 따라서 그것을 상속한 식당의 구상 Iterator 클래스도 지웁니다.
그 다음으로 Menu 인터페이스를 아래와 같이 수정합니다.

public interface Menu
{
    IEnumerator CreateIterator();
}
cs
메소드의 타입을 IEnumerator로 변경했습니다.

그리고 각 식당 Menu 클래스에서 CreateIterator()메소드를
public IEnumerator CreateIterator()
{
    return menuItems.GetEnumerator();
}
cs
로 변경해 줍니다.

웨이트리스와 테스트 코드는 새로 추가된 객체마을 카페 메뉴를 살펴보고 이어서 보여드리겠습니다.

객체마을 카페 메뉴

public class CafeMenu
{
    Hashtable menuItems = new Hashtable();
    public CafeMenu()
    {
        AddItem("베지 버거와 에어 프라이"
    "통밀빵, 상추, 감자튀김이 첨가된 베지버거"true3.99);
// 메뉴 이어짐
    }
    public void AddItem(string name, string description, 
        bool vegetarian, double price)
    {
        MenuItem menuItem = new MenuItem(name, description, 
            vegetarian, price);
        menuItems.Add(menuItem.GetName(), menuItem);
    }
     public Hashtable GetItems()
    {
        return menuItems;
    }
}
cs


코드가 고쳐지기 전의 원래 모습입니다. 따라서 Menu 인터페이스를 구현하지 않았고, Hashtable 컬렉션을 사용하여 key로는 메뉴 이름, value로는 menuItem을 사용했습니다.

이것을 위에 사용한 기존 코드의 프레임워크에 맞게 고치겠습니다.

public class CafeMenu : Menu
{
    Hashtable menuItems = new Hashtable();
    public CafeMenu()
    {
        AddItem("베지 버거와 에어 프라이"
            "통밀빵, 상추, 감자튀김이 첨가된 베지버거"true3.99);
        AddItem("오늘의 스프""샐러드가 곁들여진 오늘의 스프"false3.69);
        AddItem("베리또"
            "통 핀토 콩과 살사, 구아카몰이 곁들여진 푸짐한 베리또"true4.29);
    }
     public void AddItem(string name, string description, 
        bool vegetarian, double price)
    {
        MenuItem menuItem = new MenuItem(name, description, 
            vegetarian, price);
        menuItems.Add(menuItem.GetName(), menuItem);
    }
            
    public IEnumerator CreateIterator()
    {
        return menuItems.Values.GetEnumerator();
    }
}
cs

Menu 인터페이스를 이어받고, GetItems() 메소드는 없애고, CreateIterator() 메소드를 추가하는데 여기서 다른 것은 Hashtable 전체에 대한 객체를 리턴하는 것이 아니라 Values에 대해서만 반복자를 생성해 준다는 것입니다.

Waitress에 카페 메뉴 추가하기

이제 새롭게 바뀐 CafeMenuWaitress에 추가해 줍니다.
public class Waitress
{
    Menu pancakeHouseMenu;
    Menu dinnerMenu;
    Menu cafeMenu;
    public Waitress(Menu pancakeHouseMenu, Menu dinnerMenu, Menu cafeMenu)
    {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinnerMenu = dinnerMenu;
        this.cafeMenu = cafeMenu;
    }
    public void PrintMenu()
    {
        IEnumerator pancakeIterator = pancakeHouseMenu.CreateIterator();
        IEnumerator dinnerIterator = dinnerMenu.CreateIterator();
        IEnumerator cafeIterator = cafeMenu.CreateIterator();
        Console.WriteLine("메뉴\n----\n아침 메뉴");
        PrintMenu(pancakeIterator);
        Console.WriteLine("\n점심 메뉴");
        PrintMenu(dinnerIterator);
        Console.WriteLine("\n저녁 메뉴");
        PrintMenu(cafeIterator);
    }
    private void PrintMenu(IEnumerator iterator)
    {                
        while (iterator.MoveNext())
        {
            MenuItem menuItem = (MenuItem)iterator.Current;
            Console.Write(menuItem.GetName() + ", ");
            Console.Write(menuItem.GetPrice() + " -- ");
            Console.WriteLine(menuItem.GetDescription());
        }
    }
    // 기타 메소드
}
cs

cafeMenu가 추가되었고, 생성자에도 포함되었습니다.
이터레이터들의 타입은 모두 IEumerator로 바꾸어 주었습니다.

이제 테스트 코드를 만들어 테스트 해봅시다.
static void Main(string[] args)
{
    PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
    DinnerMenu dinnerMenu = new DinnerMenu();
    CafeMenu cafeMenu = new CafeMenu();
    Waitress waitress = new Waitress(pancakeHouseMenu, dinnerMenu, cafeMenu);
    waitress.PrintMenu();
}
cs

한 가지 유의할 점은, 이전에는 배열을 사용하는 DinnerMenuIterator 내에서 배열 전체에 값이 할당되지 않아 null이 들어갔는가를 판정해주는 부분이 있었는데 그것까지 다 지워졌다는 점입니다. 따로이 다른 곳에 판정할 곳을 마련해 주어야 하겠지만 그것까지 하기는 번거로워서 전체 배열에 다 값을 할당해 주었다는 점입니다.

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

점심 메뉴
채식주의자용 BLT, 2.99 -- 통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴
BLT, 2.99 -- 통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴
오늘의 스프, 3.29 -- 감자 샐러드를 곁들인 오늘의 스프
핫도그, 3.05 -- 사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그
찐 채소와 브라운 라이스, 3.99 -- 찐 채소와 브라운 라이스의 절묘한 조화
파스타, 3.89 -- 마리나라 소스 스파게티, 찐 채소와 효모빵도 드립니다

저녁 메뉴
베리또, 4.29 -- 통 핀토 콩과 살사, 구아카몰이 곁들여진 푸짐한 베리또
오늘의 스프, 3.69 -- 샐러드가 곁들여진 오늘의 스프
베지 버거와 에어 프라이, 3.99 -- 통밀빵, 상추, 감자튀김이 첨가된 베지버거


자, 여기까지 왔습니다.

그런데 여전히 Waitress 코드가 마음에 걸립니다. 내부에서 PrintMenu() 메소드를 여러 번 호출해야 하고, 새 메뉴가 추가될 때마다 코드를 추가해야 합니다. 이것을 개선하기 위해서는 여러 가지의 메뉴를 한꺼번에 관리할 수 있는 방법이 필요합니다.

메뉴들을 ArrayList로 묶어서 반복자로 돌린다면 여러번 PrinMenu()를 작성할 일이 없어질 겁니다. 그런데, 디저트 서브 메뉴를 추가해달라는 요청이 들어왔습니다. 메뉴 안에 또 세부 메뉴가 들어가도록 해달라는 것입니다.

그러기 위해서는

  • 메뉴, 서브메뉴, 메뉴 항목 등을 모두 집어넣을 수 있는 트리 형태의 구조가 필요합니다.
  • 각 메뉴에 있는 모든 항목에 대해 반복작업을 할 수 있는 방법을 제공해야 하며, 그 방법은 적어도 지금 사용중인 반복자 정도로 편리해야 합니다.
  • 더 유연한 방법으로 아이템에 대해서 반복작업을 할 수 있어야 합니다. 예컨대 객체마을 식당 서브 메뉴에 들어가 있는 디저트에 대해서만 반복 작업을 한다던가, 서브메뉴를 포함한 전체 메뉴에 대해서 반복작업이 가능해야 합니다.


이 문제를 해결하기 위해 또 다른 디자인 패턴을 추가하기로 합니다(여전히 이터레이터 패턴도 사용되고 있습니다).

컴포지트 패턴의 정의

객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층 구조로 만들 수 있습니다. 이 패턴을 사용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있습니다.

(여기까지 수정된 전체 코드를 공개하고 part2.로 이어집니다. )

using System;
using System.Collections;
using System.Collections.Generic;
namespace CompositePattern
{
    class Program
    {        
        // 메뉴 인터페이스
        public interface Menu
        {
            IEnumerator 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 IEnumerator CreateIterator()
            {
                return menuItems.GetEnumerator();
            }
        }
        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);
                AddItem("찐 채소와 브라운 라이스",
                    "찐 채소와 브라운 라이스의 절묘한 조화"true3.99);
                AddItem("파스타""마리나라 소스 스파게티, 찐 채소와 효모빵도 드립니다",
                    true3.89);
            }
            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 IEnumerator CreateIterator()
            {
                return menuItems.GetEnumerator();
            }
        }
        public class CafeMenu : Menu
        {
            Hashtable menuItems = new Hashtable();
            public CafeMenu()
            {
                AddItem("베지 버거와 에어 프라이"
                    "통밀빵, 상추, 감자튀김이 첨가된 베지버거"true3.99);
                AddItem("오늘의 스프""샐러드가 곁들여진 오늘의 스프"false3.69);
                AddItem("베리또"
                    "통 핀토 콩과 살사, 구아카몰이 곁들여진 푸짐한 베리또"true4.29);
            }
            public void AddItem(string name, string description, 
                bool vegetarian, double price)
            {
                MenuItem menuItem = new MenuItem(name, description, 
                    vegetarian, price);
                menuItems.Add(menuItem.GetName(), menuItem);
            }
            
            public IEnumerator CreateIterator()
            {
                return menuItems.Values.GetEnumerator();
            }
        }
        // 웨이트리스 코드
        public class Waitress
        {
            Menu pancakeHouseMenu;
            Menu dinnerMenu;
            Menu cafeMenu;
            public Waitress(Menu pancakeHouseMenu, Menu dinnerMenu, Menu cafeMenu)
            {
                this.pancakeHouseMenu = pancakeHouseMenu;
                this.dinnerMenu = dinnerMenu;
                this.cafeMenu = cafeMenu;
            }
            public void PrintMenu()
            {
                IEnumerator pancakeIterator = pancakeHouseMenu.CreateIterator();
                IEnumerator dinnerIterator = dinnerMenu.CreateIterator();
                IEnumerator cafeIterator = cafeMenu.CreateIterator();
                Console.WriteLine("메뉴\n----\n아침 메뉴");
                PrintMenu(pancakeIterator);
                Console.WriteLine("\n점심 메뉴");
                PrintMenu(dinnerIterator);
                Console.WriteLine("\n저녁 메뉴");
                PrintMenu(cafeIterator);
            }
            private void PrintMenu(IEnumerator iterator)
            {                
                while (iterator.MoveNext())
                {
                    MenuItem menuItem = (MenuItem)iterator.Current;
                    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();
            CafeMenu cafeMenu = new CafeMenu();
            Waitress waitress = new Waitress(pancakeHouseMenu, dinnerMenu, cafeMenu);
            waitress.PrintMenu();
        }
    }
}
cs

댓글 없음:

댓글 쓰기