기본 트리 구조입니다. 여기서 자식이 있는 원소는 노드(node)라고 부르고, 자식이 없는 원소는 잎(leave)라고 부릅니다. 우리의 메뉴에서 Menu는 노드, MenuItem은 잎이라고 볼수 있겠지요. 이것들을 조합하면 복잡한 트리를 만들 수 있습니다.
그러나 이 복합구조(composite structure)는 결국에 가장 기본 구조인 노드와 잎의 관계로서 표현하고 작업할 수 있습니다. 이쯤에서 컴포짓 패턴의 다이어그램을 살펴 보겠습니다.
Client는 Component라는 인터페이스를 사용하여 복합객체 내의 객체들을 이용할 수 있습니다.
Component에서는 복합 객체 내에 들어있는 모든 객체들에 대한 인터페이스를 정의합니다. 노드와 잎에 대한 메소드들 말이죠.
Composite에서는 자식이 있는 구성요소의 행동을 정의하고 자식 구성요소를 저장하는 역할을 합니다.
Leaf는 자식이 없고, 그 원소가 해야할 행동을 정의하여 구현합니다.
컴포짓 패턴을 이용한 메뉴 디자인
이 다이어그램에 맞게 구현을 시작해보겠습니다.
MenuComponent 구현
public abstract class MenuComponent
{
public virtual void Add(MenuComponent menuComponent)
{
throw new NotSupportedException();
}
public virtual void Remove(MenuComponent menuComponent)
{
throw new NotSupportedException();
}
public virtual MenuComponent GetChild(int i)
{
throw new NotSupportedException();
}
public virtual string GetName()
{
throw new NotSupportedException();
}
public virtual string GetDescription()
{
throw new NotSupportedException();
}
public virtual double GetPrice()
{
throw new NotSupportedException();
}
public virtual bool IsVegetarian()
{
throw new NotSupportedException();
}
public virtual void Print()
{
throw new NotSupportedException();
}
}
| cs |
MenuItem 구현
MenuItem은 다이어그램 상 잎에 해당하며, 복합 객체의 원소에 해당하는 행동을 구현해야 하는 클래스입니다.
public class MenuItem : MenuComponent
{
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 override string GetName()
{
return name;
}
public override string GetDescription()
{
return description;
}
public override double GetPrice()
{
return price;
}
public override bool IsVegetarian()
{
return vegetarian;
}
public override void Print()
{
Console.Write(" " + GetName());
if (IsVegetarian())
{
Console.Write("(v)");
}
Console.WriteLine(", " + GetPrice());
Console.WriteLine(" -- " + GetDescription());
}
}
| cs |
Menu 구현
이제 복합객체 클래스인 메뉴를 구현합니다. 이전 이터레이터 패턴에서는 인터페이스로 처리 했었지만 여기서는 클래스가 됩니다. 그보다 상위인 MenuComponent 추상 클래스가 있기 때문입니다. 그 안의 메소드 중 GetPrice(), IsVegetarian()은 메뉴에서는 의미가 없으므로 구현하지 않았습니다.
public class Menu :MenuComponent
{
List<MenuComponent> menuComponents = new List<MenuComponent>();
string name;
string description;
public Menu(string name,string description)
{
this.name = name;
this.description = description;
}
public override void Add(MenuComponent menuComponent)
{
menuComponents.Add(menuComponent);
}
public override void Remove(MenuComponent menuComponent)
{
menuComponents.Remove(menuComponent);
}
public override MenuComponent GetChild(int i)
{
return (MenuComponent)menuComponents[i];
}
public override string GetName()
{
return name;
}
public override string GetDescription()
{
return description;
}
public override void Print()
{
Console.Write("\n" + GetName());
Console.WriteLine(", " + GetDescription());
Console.WriteLine("----------------------");
}
}
| cs |
MenuComponent 형식의 자식을 저장하기 위해 List를 준비하여 Add, Remove, GetChild 메소드로 다루게 하였습니다.
여기서 한 가지, Print() 메소드에 문제가 있습니다. 메뉴는 복합객체이므로 MenuItem과 Menu가 모두 들어있을 수 있습니다. 그러므로, Print() 호출 시 그 내부의 모든 구성요소들이 출력되어야 합니다. 각 구성요소들이 자기 자신을 출력하는 법을 알고 있으므로 재귀적인 방법으로 하위의 정보까지 모두 출력하도록 수정해야 합니다.
반복자를 사용하여 위의 Print 메소드를 고쳐보도록 하겠습니다.
public override void Print()
{
Console.Write("\n" + GetName());
Console.WriteLine(", " + GetDescription());
Console.WriteLine("----------------------");
//추가
IEnumerator enumerator = menuComponents.GetEnumerator();
while (enumerator.MoveNext())
{
MenuComponent menuComponent = (MenuComponent)enumerator.Current;
menuComponent.Print();
}
}
| cs |
while이 돌아가면서 다른 메뉴가 나타나면 또 재귀적으로 그 메뉴에 대한 반복작업을 수행하게 됩니다.
Waitress 구현
위의 패턴을 사용할 클라이언트인 Waitress를 구현해 보겠습니다.
public class Waitress
{
MenuComponent allMenus;
public Waitress(MenuComponent allMenus)
{
this.allMenus = allMenus;
}
public void PrintMenu()
{
allMenus.Print();
}
}
| cs |
전에 비해 매우 간단해졌습니다.
이제 테스트를 작성해 보겠습니다.
중간에 //메뉴 항목 추가하는 코드 ... 부분에 들어갈 메뉴들은 하드 코딩이므로 여기서는 간단한 예만 보여드리고 나머지는 생략할 겁니다. 전체 코드를 보고 싶으신 부분은 글의 가장 하단을 참고해 보세요.
static void Main(string[] args)
{
MenuComponent pancakeHouseMenu = new Menu("팬케이크 하우스 메뉴", "아침 메뉴");
MenuComponent dinerMenu = new Menu("객체마을 식당 메뉴", "점심 메뉴");
MenuComponent cafeMenu = new Menu("카페 메뉴", "저녁 메뉴");
MenuComponent dessertMenu = new Menu("디저트 메뉴", "디저트를 즐겨 보세요!");
MenuComponent allMenus = new Menu("전체 메뉴", "전체 메뉴");
allMenus.Add(pancakeHouseMenu);
allMenus.Add(dinerMenu);
allMenus.Add(cafeMenu);
//메뉴 항목 추가하는 코드
dinerMenu.Add(new MenuItem(
"Pasta",
"Spaghetti with Marinara Sauce, and a slice of sourdough bread",
true,
3.89));
dinerMenu.Add(dessertMenu);
dessertMenu.Add(new MenuItem(
"Apple Pie",
"Apple pie with a flakey crust, topped with vanilla icecream",
true,
1.59));
// 메뉴 항목 추가하는 코드
Waitress waitress = new Waitress(allMenus);
waitress.PrintMenu();
}
| cs |
결과)
전체 메뉴, 전체 메뉴
----------------------
팬케이크 하우스 메뉴, 아침 메뉴
----------------------
K&B's Pancake Breakfast(v), 2.99
-- Pancakes with scrambled eggs, and toast
Regular Pancake Breakfast, 2.99
-- Pancakes with fried eggs, sausage
Blueberry Pancakes(v), 3.49
-- Pancakes made with fresh blueberries, and blueberry syrup
Waffles(v), 3.59
-- Waffles, with your choice of blueberries or strawberries
객체마을 식당 메뉴, 점심 메뉴
----------------------
Vegetarian BLT(v), 2.99
-- (Fakin') Bacon with lettuce & tomato on whole wheat
BLT, 2.99
-- Bacon with lettuce & tomato on whole wheat
Soup of the day, 3.29
-- A bowl of the soup of the day, with a side of potato salad
Hotdog, 3.05
-- A hot dog, with saurkraut, relish, onions, topped with cheese
Steamed Veggies and Brown Rice(v), 3.99
-- Steamed vegetables over brown rice
Pasta(v), 3.89
-- Spaghetti with Marinara Sauce, and a slice of sourdough bread
디저트 메뉴, 디저트를 즐겨 보세요!
----------------------
Apple Pie(v), 1.59
-- Apple pie with a flakey crust, topped with vanilla icecream
Cheesecake(v), 1.99
-- Creamy New York cheesecake, with a chocolate graham crust
Sorbet(v), 1.89
-- A scoop of raspberry and a scoop of lime
카페 메뉴, 저녁 메뉴
----------------------
Veggie Burger and Air Fries(v), 3.99
-- Veggie burger on a whole wheat bun, lettuce, tomato, and fries
Soup of the day, 3.69
-- A cup of the soup of the day, with a side salad
Burrito(v), 4.29
-- A large burrito, with whole pinto beans, salsa, guacamole
계속하려면 아무 키나 누르십시오 . . .
지금까지는 한 클래스에서 한 역할만 해야 한다고 강조해왔는데 컴포짓 패턴에서는 계층관리와 메뉴관련 작업 두가지를 수행합니다. 컴포짓 패턴은 단일 역할 원칙을 깨면서 투명성(transparency)을 확보하기 위해 두 가지 역할을 넣었다고 할 수 있습니다. 자식관리의 역할과 잎으로서의 기능을 모두 넣어서 똑같은 방식으로 처리하기 위함입니다. 잎은 결국 자식이 0인 노드이니까요.
이터레이터 추가
웨이트리스에서 채식주의자용 메뉴 항목만 뽑아낸다던가 하는 경우에 이터레이터 패턴을 결합하여 반복작업을 수행할 수 있습니다.이를 위해 먼저 모든 구성요소에 CreateIterator()메소드를 첨가하겠습니다.
MenuComponent에 제일 먼저 추가해야 하겠죠. 전과 동일하게 익셉션을 던지는 기본 메소드를 추가했습니다. 그렇게 되면 Menu, MenuItem에서도 모두 구현해야 합니다.
public class Menu : MenuComponent
{
// 앞은 그대로...
public override IEnumerator CreateIterator()
{
if (enumerator == null)
{
enumerator = new CompositeIterator(menuComponents.GetEnumerator());
}
return enumerator;
}
}
| cs |
public class MenuItem : MenuComponent
{
// 앞은 그대로...
public override IEnumerator CreateIterator()
{
return new NullIterator();
}
}
| cs |
복합반복자
갑자기 튀어나온 복합반복자인 CompositeIterator를 구현해 보겠습니다. 이것은 복합 객체 내에 들어 있는 MenuItem에 대해 반복작업을 할 수 있게 해줍니다. 내부에 재귀의 개념이 들어가서 좀 이해하기 어려울 수가 있습니다.
public class CompositeIterator : IEnumerator
{
Stack stack = new Stack();
public CompositeIterator(IEnumerator enumerator)
{
stack.Push(enumerator);
}
public bool MoveNext()
{
if (stack.Count == 0)
{
return false;
}
else
{
IEnumerator enumerator = (IEnumerator)stack.Peek();
if (!enumerator.MoveNext())
{
stack.Pop();
return MoveNext();
}
else
{
return true;
}
}
}
public object Current
{
get
{
if (stack.Count == 0)
{
return null;
}
else
{
IEnumerator enumerator = (IEnumerator)stack.Peek();
MenuComponent component = (MenuComponent)enumerator.Current;
if (component is Menu)
{
stack.Push(component.CreateIterator());
}
return component;
}
}
}
public void Reset()
{
}
}
| cs |
역시 IEnumerator를 사용했습니다. 생성자에서 받아온 복합 객체를 스택에 밀어넣어 줍니다. 이 부분이 역시 자바와 많이 달라서 또 한참 헤맸는데, IEnumerator 구현시에 작성해야 하는 메소드는 MoveNext(), Reset()이고, Current라는 property를 구현해야 합니다. MoveNext() 구현시 내부에 재귀가 들어갑니다. 받아온 객체가 Menu라면 다시 스택에 넣어서 남은 것이 없을 때까지 반복되는 것이죠. 구현할 필요가 없는 Reset은 그냥 두었습니다.
널 반복자
Menu가 아닌 MenuItem의 경우 반복작업을 할 대상이 없습니다. CreateIterator()는 구현해야 하므로 이 때 사용 가능한 방법은 두 가지가 있습니다.
첫째, CreateIterator에서 그냥 널을 리턴한다.
이 방법 사용시에는 클라이언트에서 리턴된 값이 널인지 아닌지를 판단하는 조건문을 사용해야 하는 단점이 있습니다.
둘째, MoveNext() 호출시 무조건 false를 리턴하는 반복자를 리턴한다.
이 방법을 사용하기로 합니다.
public class NullIterator : IEnumerator
{
public object Current
{
get { return null; }
}
public bool MoveNext()
{
return false;
}
public void Reset()
{
}
}
| cs |
채식주의자용 메뉴
Waitress에 채식 주의자용 메뉴를 출력하는 메소드를 추가합니다.
public class Waitress
{
// 앞 부분 동일
public void PrintVegetarianMenu()
{
IEnumerator enumerator = allMenus.CreateIterator();
Console.WriteLine("\nVEGETARIAN MENU\n----");
while (enumerator.MoveNext())
{
MenuComponent menuComponent = (MenuComponent)enumerator.Current;
try
{
if (menuComponent.IsVegetarian())
{
menuComponent.Print();
}
}
catch(NotSupportedException e) { }
}
}
}
| cs |
allMenus를 받아서 반복작업을 수행하면서 IsVegetarian이 true일 때만 Print()를 호출합니다.
이제 테스트 해보겠습니다.
결과)
VEGETARIAN MENU
----
K&B's Pancake Breakfast(v), 2.99
-- Pancakes with scrambled eggs, and toast
Blueberry Pancakes(v), 3.49
-- Pancakes made with fresh blueberries, and blueberry syrup
Waffles(v), 3.59
-- Waffles, with your choice of blueberries or strawberries
Vegetarian BLT(v), 2.99
-- (Fakin') Bacon with lettuce & tomato on whole wheat
Steamed Veggies and Brown Rice(v), 3.99
-- Steamed vegetables over brown rice
Pasta(v), 3.89
-- Spaghetti with Marinara Sauce, and a slice of sourdough bread
Apple Pie(v), 1.59
-- Apple pie with a flakey crust, topped with vanilla icecream
Cheesecake(v), 1.99
-- Creamy New York cheesecake, with a chocolate graham crust
Sorbet(v), 1.89
-- A scoop of raspberry and a scoop of lime
Veggie Burger and Air Fries(v), 3.99
-- Veggie burger on a whole wheat bun, lettuce, tomato, and fries
Burrito(v), 4.29
-- A large burrito, with whole pinto beans, salsa, guacamole
계속하려면 아무 키나 누르십시오 . . .
이터레이터와 컴포짓 패턴을 공부하는데 상당히 힘과 시간이 많이 들었네요. 우여곡절 끝에 마치게 되니 뿌듯합니다. 마지막으로 전체 코드를 공개하고 마칠까 합니다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CompositePattern
{
class Program
{
// 메뉴컴포넌트 추상 클래스
public abstract class MenuComponent
{
public virtual void Add(MenuComponent menuComponent)
{
throw new NotSupportedException();
}
public virtual void Remove(MenuComponent menuComponent)
{
throw new NotSupportedException();
}
public virtual MenuComponent GetChild(int i)
{
throw new NotSupportedException();
}
public virtual string GetName()
{
throw new NotSupportedException();
}
public virtual string GetDescription()
{
throw new NotSupportedException();
}
public virtual double GetPrice()
{
throw new NotSupportedException();
}
public virtual bool IsVegetarian()
{
throw new NotSupportedException();
}
public virtual void Print()
{
throw new NotSupportedException();
}
public virtual IEnumerator CreateIterator()
{
throw new NotSupportedException();
}
}
// 메뉴 클래스
public class Menu : MenuComponent
{
IEnumerator enumerator = null;
List<MenuComponent> menuComponents = new List<MenuComponent>();
string name;
string description;
public Menu(string name,string description)
{
this.name = name;
this.description = description;
}
public override void Add(MenuComponent menuComponent)
{
menuComponents.Add(menuComponent);
}
public override void Remove(MenuComponent menuComponent)
{
menuComponents.Remove(menuComponent);
}
public override MenuComponent GetChild(int i)
{
return menuComponents[i];
}
public override string GetName()
{
return name;
}
public override string GetDescription()
{
return description;
}
public override void Print()
{
Console.Write("\n" + GetName());
Console.WriteLine(", " + GetDescription());
Console.WriteLine("----------------------");
IEnumerator enumerator = menuComponents.GetEnumerator();
while (enumerator.MoveNext())
{
MenuComponent menuComponent = (MenuComponent)enumerator.Current;
menuComponent.Print();
}
}
public override IEnumerator CreateIterator()
{
if (enumerator == null)
{
enumerator = new CompositeIterator(menuComponents.GetEnumerator());
}
return enumerator;
}
}
// 공통 메뉴 코드
public class MenuItem : MenuComponent
{
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 override string GetName()
{
return name;
}
public override string GetDescription()
{
return description;
}
public override double GetPrice()
{
return price;
}
public override bool IsVegetarian()
{
return vegetarian;
}
public override void Print()
{
Console.Write(" " + GetName());
if (IsVegetarian())
{
Console.Write("(v)");
}
Console.WriteLine(", " + GetPrice());
Console.WriteLine(" -- " + GetDescription());
}
public override IEnumerator CreateIterator()
{
return new NullIterator();
}
}
// 복합 객체 이터레이터
public class CompositeIterator : IEnumerator
{
Stack stack = new Stack();
//private List<MenuComponent> menuComponents;
//int position = -1;
public CompositeIterator(IEnumerator enumerator)
{
stack.Push(enumerator);
}
public bool MoveNext()
{
if (stack.Count == 0)
{
return false;
}
else
{
IEnumerator enumerator = (IEnumerator)stack.Peek();
if (!enumerator.MoveNext())
{
stack.Pop();
return MoveNext();
}
else
{
return true;
}
}
}
public object Current
{
get
{
if (stack.Count == 0)
{
return null;
}
else
{
IEnumerator enumerator = (IEnumerator)stack.Peek();
MenuComponent component = (MenuComponent)enumerator.Current;
if (component is Menu)
{
stack.Push(component.CreateIterator());
}
return component;
}
}
}
public void Reset()
{
}
}
// 널 이터레이터
public class NullIterator : IEnumerator
{
public object Current
{
get { return null; }
}
public bool MoveNext()
{
return false;
}
public void Reset()
{
}
}
// 웨이트리스 코드
public class Waitress
{
MenuComponent allMenus;
public Waitress(MenuComponent allMenus)
{
this.allMenus = allMenus;
}
public void PrintMenu()
{
allMenus.Print();
}
public void PrintVegetarianMenu()
{
IEnumerator enumerator = allMenus.CreateIterator();
Console.WriteLine("\nVEGETARIAN MENU\n----");
while (enumerator.MoveNext())
{
MenuComponent menuComponent = (MenuComponent)enumerator.Current;
try
{
if (menuComponent.IsVegetarian())
{
menuComponent.Print();
}
}
catch(NotSupportedException e)
{
}
}
}
}
static void Main(string[] args)
{
MenuComponent pancakeHouseMenu = new Menu("팬케이크 하우스 메뉴", "아침 메뉴");
MenuComponent dinerMenu = new Menu("객체마을 식당 메뉴", "점심 메뉴");
MenuComponent cafeMenu = new Menu("카페 메뉴", "저녁 메뉴");
MenuComponent dessertMenu = new Menu("디저트 메뉴", "디저트를 즐겨 보세요!");
MenuComponent allMenus = new Menu("전체 메뉴", "전체 메뉴");
allMenus.Add(pancakeHouseMenu);
allMenus.Add(dinerMenu);
allMenus.Add(cafeMenu);
//메뉴 항목 추가하는 코드
pancakeHouseMenu.Add(new MenuItem(
"K&B's Pancake Breakfast",
"Pancakes with scrambled eggs, and toast",
true,
2.99));
pancakeHouseMenu.Add(new MenuItem(
"Regular Pancake Breakfast",
"Pancakes with fried eggs, sausage",
false,
2.99));
pancakeHouseMenu.Add(new MenuItem(
"Blueberry Pancakes",
"Pancakes made with fresh blueberries, and blueberry syrup",
true,
3.49));
pancakeHouseMenu.Add(new MenuItem(
"Waffles",
"Waffles, with your choice of blueberries or strawberries",
true,
3.59));
dinerMenu.Add(new MenuItem(
"Vegetarian BLT",
"(Fakin') Bacon with lettuce & tomato on whole wheat",
true,
2.99));
dinerMenu.Add(new MenuItem(
"BLT",
"Bacon with lettuce & tomato on whole wheat",
false,
2.99));
dinerMenu.Add(new MenuItem(
"Soup of the day",
"A bowl of the soup of the day, with a side of potato salad",
false,
3.29));
dinerMenu.Add(new MenuItem(
"Hotdog",
"A hot dog, with saurkraut, relish, onions, topped with cheese",
false,
3.05));
dinerMenu.Add(new MenuItem(
"Steamed Veggies and Brown Rice",
"Steamed vegetables over brown rice",
true,
3.99));
dinerMenu.Add(new MenuItem(
"Pasta",
"Spaghetti with Marinara Sauce, and a slice of sourdough bread",
true,
3.89));
dinerMenu.Add(dessertMenu);
dessertMenu.Add(new MenuItem(
"Apple Pie",
"Apple pie with a flakey crust, topped with vanilla icecream",
true,
1.59));
dessertMenu.Add(new MenuItem(
"Cheesecake",
"Creamy New York cheesecake, with a chocolate graham crust",
true,
1.99));
dessertMenu.Add(new MenuItem(
"Sorbet",
"A scoop of raspberry and a scoop of lime",
true,
1.89));
cafeMenu.Add(new MenuItem(
"Veggie Burger and Air Fries",
"Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
true,
3.99));
cafeMenu.Add(new MenuItem(
"Soup of the day",
"A cup of the soup of the day, with a side salad",
false,
3.69));
cafeMenu.Add(new MenuItem(
"Burrito",
"A large burrito, with whole pinto beans, salsa, guacamole",
true,
4.29));
Waitress waitress = new Waitress(allMenus);
waitress.PrintMenu();
waitress.PrintVegetarianMenu();
}
}
}
| cs |
댓글 없음:
댓글 쓰기