
이번에 공부해본 디자인 패턴은 '데코레이터 패턴' 이다.
말 그대로 장식하고 꾸며주는 패턴이다.
처음 읽을때는 이해가 잘 안돼서 여러번 반복해서 읽다가 코드를 보고 어느정도 이해를 하게 됐다.
책에 나온 예제는 '스타버스' 라고 하는 카페의 메뉴 결제 시스템에 대한 이야기가 나온다.
커피에도 종류가 있고, 또 각자의 입맛에 따라 시럽,두유,우유 등등.. 여러가지를 추가 할 수 있다.
먼저 이렇게 추가가 될 수 있는 것들이 정해져 있지 않은 , 즉 결제를 할 때마다 사람에 따라 모든 선택지(메뉴)가 달라질수도 있는 시스템을 만들때 어떻게 코드를 짤 것인가 라고 생각을 해보면 마냥 쉽지만은 않다.
똑같이 카페를 예제로 코드를 만들면 재미가 없으니 떡볶이 가게로 한번 예를 들어볼까 한다.
JW의 떡볶이 가게
JW의 떡볶이 가게에는 일반 떡볶이, 매운 떡볶이, 로제 떡볶이를 팔고있다.
그리고 당면 , 치즈 , 라면사리, 계란 이렇게 4개의 재료들을 돈만 낸다면 마음껏, 양껏 추가 가능하다.
Food 클래스 ( = 떡볶이 클래스)
public abstract class Food
{
protected string Description;
public string GetDescription()
{
return Description;
}
public abstract double Cost();
}
편의상 떡볶이 클래스를 Food 클래스로 만들었다.
Food라는 추상 클래스는 변수로 어떤 떡볶이인지에 대한 설명과 그 설명의 게터 메소드를 갖고있다.
가격은 각자 다르기 때문에 서브 클래스에서 직접 구현 하도록 했다.
기본,매운,로제 떡볶이 클래스
public class Basic : Food
{
public Basic()
{
Description = "일반 떡볶이";
}
public override double Cost()
{
return 12000;
}
}
public class Spicy : Food
{
public Spicy()
{
Description = "매운 떡볶이";
}
public override double Cost()
{
return 13000;
}
}
public class Rose : Food
{
public Rose()
{
Description = "로제 떡볶이";
}
public override double Cost()
{
return 14000;
}
}
Food라는 클래스를 상속받은 기본,매운,로제 떡볶이들의 생성자에서 어떤 떡볶이인지 입력해주고, 떡볶이에 따라 맞는 가격을 리턴 하도록 했다.
class MainApp
{
static void Main(string[] args)
{
Food food = new Rose();
Console.WriteLine(food.GetDescription());
Console.WriteLine(food.Cost().ToString("N0") + " 원");
}
}
이렇게 로제 떡볶이 클래스를 생성해서 이용하면 알맞은 이름과 금액이 뜨는것을 확인 할 수 있다.
추가 재료들은 어떻게 계산 할까?
책의 예제에서는 다른 재료들을 추가해서 계산하는 기능을 추가 할 때 , 이렇게 하면 안된다는 예제로 RoseWithEgg / SpicyWithCheese 등등 수많은 서브 클래스들이 만들어지는 상황을 제시한다.
그래서 다른 예시로는 Food 클래스에 재료들 마다 bool 변수를 선언하고, 이 변수의 게터 세터 메소드를 만든다.
이 메소들을 이용해서 서브 클래스의 Cost 메소드 안에서 따로 처리를 하여 값을 계산한다.
코드로 보자면 이런식이다.
public abstract class Food
{
protected string Description;
bool egg;
bool cheese;
public bool HasEgg()
{
return egg;
}
public bool HasCheese()
{
return cheese;
}
}
편의상 짧게 쳤지만 이런식으로 bool 값을 이용해서 게터 메소들을 만들고,
public class Basic : Food
{
public Basic()
{
Description = "일반 떡볶이";
}
public override double Cost()
{
double cost = 12000;
if(HasEgg())
{
cost += 1000;
}
if(HasCheese())
{
cost += 2000;
}
return cost;
}
}
재정의 해야하는 Cost 메소드에서 게터 메소드를 이용해서 추가한 재료만큼 값을 더해주는 방식이다.
딱 보기에도 이렇게 하면 안될 것 같은 느낌이 오는 코드다.
개방 - 폐쇄 원칙 [ OCP (Open-Closed-Principle) ]
클래스는 확장에는 열려있어야 하지만,
변경에는 닫혀 있어야 한다.
면접을 위해 달달 외웠었던 SOLID 객체 지향 설계 원칙중에 하나 인데,
확장엔 열려있고, 변경에는 닫혀 있어야 한다는 원칙이다.
(열심히 외웠지만 면접에서 아무도 물어보진 않았다.)
바로 위의 코드처럼 재료에 따라 값을 추가하는 코드를 짠다면, 당장은 굴러 가겠지만 새로운 재료가 생겼을때나 재료의 가격이 변동 되었을때 수정해야하는 것이 힘들어진다.
새로운 재료가 추가나 기존 재료의 값이 변경 되었다고 가정을 해본다면 ..
1. Food 클래스에 bool값도 추가
2. 추가한 재료의 게터,세터 메소드 추가
3. 떡볶이마다 있는 Cost메소드에 가서 새로운 재료의 계산 코드 추가
4. 가격이 변경되었다면 가격까지 변경
그리고 같은 재료를 2개 추가했을때 적용시키기 정말 힘들어진다.
데코레이터 패턴으로 객체를 감싸보자
OCP 원칙을 지키고 조금 더 효율적인 코드를 위해 데코레이터 패턴을 사용 하자.
기본적인 떡볶이가 있고, 우리는 이 떡볶이에 재료들을 감싸서 '장식'을 해보면 된다.
Food 클래스 (추상 구성요소)
public abstract class Food
{
protected string Description;
public virtual string GetDescription()
{
return Description;
}
public abstract double Cost();
}
가장 상위클래스를 담당할 Food 클래스이다.
Description 의 게터 메소드만 오버라이드가 가능하게 virtual 한정자를 넣어주었고,
Cost는 똑같이 하위 클래스에서 재정의 하도록 추상 메소드로 만들어 주었다.
Ingredient 클래스 (추상 구성요소)
public abstract class ingredient : Food
{
//감쌀 객체
protected Food food;
public override abstract string GetDescription();
}
새롭게 재료 클래스를 만들어주었다.
똑같이 Food 클래스를 상속 받고 있으며, 변수로 Food 클래스의 객체를 가지고 있어야 한다.
=> 이 객체가 우리가 감싸서 장식을 해줄 객체다.
또 Food클래스의 GetDescription 메소드를 재정의 하는 추상 메소드를 만들어주었다.
기본,매운,로제 떡볶이 클래스 (구상 구성요소)
public class Basic : Food
{
public Basic()
{
Description = "일반 떡볶이";
}
public override double Cost()
{
return 12000;
}
}
전과 달라진게 없이 어떤 떡볶이 인지에 대한 생성자와 가격을 리턴하는 메소드가 있다.
(매운,로제 떡볶이 클래스는 가격만 빼면 코드가 완전 똑같아서 생략)
달걀,치즈,라면사리,당면 클래스 (구상 구성요소)
public class Egg : ingredient
{
public Egg(Food food)
{
this.food = food;
}
public override double Cost()
{
return food.Cost() + 1000;
}
public override string GetDescription()
{
return food.GetDescription() + ", 계란";
}
}
생성자에는 Food 클래스의 인스턴스를 받아서 변수로 가지고 있게 한다.
=> 즉 , 우리가 감싸서 꾸며줄 인스턴스
재정의 한 Cost 메소드에서는 생성자에서 받은 Food 클래스 인스턴스의 가격에 달걀 가격인 천원을 넣어서 리턴한다.
그리고 GetDescription 메소드에서도 똑같이 생성자에서 받은 Food 클래스 인스턴스의 설명에 달걀을 넣어서 리턴한다.
(치즈,라면사리,당면 클래스도 가격과 이름만 달라서 생략)
객체를 신나게 꾸며보기
class MainApp
{
static void Main(string[] args)
{
Food food = new Rose();
Console.WriteLine(food.GetDescription());
Console.WriteLine(food.Cost().ToString("N0") + " 원");
food = new Egg(food);
food = new Egg(food);
Console.WriteLine(food.GetDescription());
Console.WriteLine(food.Cost().ToString("N0") + " 원");
food = new RamenNoodle(food);
Console.WriteLine(food.GetDescription());
Console.WriteLine(food.Cost().ToString("N0") + " 원");
}
}
1. 로제 떡볶이를 생성한다.
2. 로제 떡볶이에 재료들을 생성해서 로제떡볶이를 생성자 인자로 바로 넘긴다.
이렇게 하면 바로바로 떡볶이 객체에 재료 객체들이 감싸며 꾸며주어서 꾸민 이후의 가격과 설명들이 리턴되어 표시된다.
재료나 떡볶이가 추가되어도 클래스만 만들어주면 끝이기에 따로 수정이 필요가 없다.
설명을 보아하니 이 패턴 자체가 가지는 단점이 조금 있는데,
1. 자잘한 클래스가 엄청나게 추가되는 경우.
2. 특정 형식에 의존하는 클라이언트 코드를 가져와서 생각도 없이 데코레이터 패턴을 적용하는 경우.
3. 이 패턴을 도입하면 구성 요소를 초기화 하는데 필요한 코드가 훨씬 복잡해진다는 단점.
등등.. 이 있다고 하지만 아직 많이 써보지 않아서 잘 와닿지는 않는다.
아픙로 배울 팩토리와 빌더 패턴과 함께 사용하면 더더욱 완성도 있는 패턴이 된다고 하니 다른 패턴도 빨리 공부를 해봐야겠다.
'프로그래밍 공부 > 디자인 패턴' 카테고리의 다른 글
싱글톤 패턴 vs 정적 클래스 (Singleton Pattern vs Static Class) (0) | 2024.02.29 |
---|---|
디자인 패턴 - <옵저버 패턴> (Observer Pattern) (0) | 2022.11.20 |
디자인 패턴 - <전략 패턴> (Strategy Pattern) (2) | 2022.11.14 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!