데코레이터 패턴
기존의 객체에 동적으로 새로운 기능을 추가할 수 있게 해주는 패턴이다. 새로운 기능을 추가한 서브클래스를 생성하는 것보다 융통성 있는 방법을 제공한다. 본래의 기능을 새로운 기능이 감싸안음으로써 기능을 더할 수 있다.
구조
- Component - 동적으로 추가할 서비스를 가질 가능성이 있는 객체들에 대한 인터페이스
- ConcreteComponent - 추가적인 서비스가 실제로 정의되어야 할 필요가 있는 객체
- Decorator - 객체에 대한 참조자를 관리하면서 Component에 정의된 인터페이스를 만족하도록 인터페이스를 정의
- ComcreteDecorator - Component에 새롭게 추가할 서비스를 실제로 구현하는 클래스
기본 인터페이스
public interface Component
{
public void Operation();
}
public class ConcreteComponent : Component
{
public void Operation()
{
Console.Write("ConcreteComponent Operation!");
}
}
public abstract class Decorator : Component
{
protected Component concreteDecorator;
public Decorator(Component concreteDecorator)
{
this.concreteDecorator = concreteDecorator;
}
public abstract void Operation();
}
public class ConcreteDecoratorA : Decorator
{
public ConcreteDecoratorA(Component conceteDecorator) : base(conceteDecorator)
{
}
public override void Operation()
{
Console.Write("ConcreteDecoratorA Operation with ");
base.concreteDecorator.Operation();
}
}
public class ConcreteDecoratorB : Decorator
{
public ConcreteDecoratorB(Component conceteDecorator) : base(conceteDecorator)
{
}
public override void Operation()
{
Console.Write("ConcreteDecoratorB Operation with ");
base.concreteDecorator.Operation();
}
}
public class MainTest
{
public static int Main(string[] args)
{
Component component = new ConcreteDecoratorA(new ConcreteDecoratorB(new ConcreteComponent()));
component.Operation();
return 0;
}
}
ConcreteDecoratorA Operation with ConcreteDecoratorB Operation with ConcreteComponent Operation!
왜 사용해야 할까?
기존 기능에 새로운 기능을 추가하는 방식 중 가장 생각하기 편한 방식은 상속, 즉 자식 클래스를 생성하고 함수를 오버라이딩 혹은 추가 함수를 작성하는 방식일 것이다. 그런데 만약 필요에 따라 추가되는 기능이 여러개일 경우엔 어떻게 될까?
예를 들어 기본 무기인 검(BaseSword)가 있다고 하자. 이후 검에서 추가적인 기능인 화염(Fire), 출혈(Bleed), 연속 공격하는 기능(Twin)등이 달린 검들을 추가하려고 한다. 화염, 출혈, 연속 공격 등은 이미 정의된 클래스가 있기 때문에 해당 클래스들을 상속받아서 만들었다.
그런데 화염, 출혈의 기능들이 동시에 달린 검을 만든다면? 무기가 도끼(Axe)가 만들어진다면? 객체지향에선 클래스의 모호성을 방지하기 위해 인터페이스를 제외한 다중상속을 방지하고 있고, 된다고 하더라도 너무나도 많은 클래스들을 만들어내야 할 것이고, 작성할 코드도 방대해지기 때문에 유지보수가 최악일 것이다.
이러한 문제가 발생하는 이유는 상속의 문제점 때문이다.
- 상속은 정적이다. 런타임 동안 기존 객체의 행동을 변경할 수 없다.
- 자식 클래스는 하나의 부모 클래스만 가질 수 있다.
이러한 문제를 극복하는 방법은 바로 상속이 아닌 집합, 구성 관계로 클래스를 작성하는 것이다. 집합, 구성은 상속처럼 자식 클래스가 부모 클래스에 종속성을 갖고 있는 대신 한 객체가 다른 객체에 대한 참조를 갖고 일부 작업을 위임하기 때문에 종속성이 낮다. 이러한 방식을 사용하면 객체는 필요에 따라 여러 클래스들을 사용할 수도, 사용하는 클래스를 동적으로 변경할 수도 있다.
데코레이터는 구성 관계를 통해 기능들을 좀 더 유연하게 확장할 수 있도록 도움을 준다. 추가해야 할 기능들이 많을 경우, 추가해야 할 기능들을 데코레이터 클래스로 정의하고, 데코레이터들은 자신의 기능과 함께 또 다른 데코레이터들을 참조할 수 있다. 이에 따라 필요한 경우에 따라 추가하려면 기존의 데코레이터 클래스를 추가 기능인 데코레이터 클래스가 감싸안아 참조한다. 이를 래핑(wrapping)이라 하며, 이를 통해 추가적인 클래스의 변경 없이 다양한 조합을 만들어낼 수 있으며, 런타임 도중에 기능을 추가할 수도 있다.
class Client
{
static void Main()
{
// Sword 객체 생성
Sword basicSword = new Sword();
// Fire, Bleed 효과 추가
basicSword.AddEffect(new Fire(new Bleed()));
//Twin 효과 추가
basicSword.AddEffect(new Twin());
// 공격 시 효과들이 실행됨
basicSword.Attack();
}
}
장점
- 클래스의 추가 없이 다양한 기능을 조합할 수 있다.
- 런타임 중 기능을 동적으로 추가할 수 있다.
- 클래스가 필요한 기능(책임)만을 가질 수 있다(SRP)
- 클래스 개발 시 현재 사용되지 않는 기능까지 고려하여 시간과 노력을 투자할 필요가 없어진다. 필요하면 데코레이터로 추가하면 된다.
단점
- 여러 번 래핑할 경우 특정 래핑한 기능을 제거하기가 어렵다.
- 데코레이터의 행동이 데코레이터에서 래핑한 순서, 즉 스택의 순서에 의존하지 않도록 구현하는 것이 어렵다.
전체 코드
Decorator(Effect), ConcreteDecorator(Fire, Bleed, Twin)
abstract class Effect
{
protected Effect nextEffect;
public Effect(Effect nextEffect)
{
this.nextEffect = nextEffect;
}
public abstract void UseEffect();
}
// Fire, Bleed, Twin 클래스들은 Effect의 서브 클래스
class Fire : Effect
{
public Fire(Effect nextEffect) : base(nextEffect) { }
public override void UseEffect()
{
Console.WriteLine("Fire Enchanted!");
if (nextEffect != null)
{
nextEffect.UseEffect();
}
}
}
class Bleed : Effect
{
public Bleed(Effect nextEffect) : base(nextEffect) { }
public override void UseEffect()
{
Console.WriteLine("Bleed Enchanted!");
if (nextEffect != null)
{
nextEffect.UseEffect();
}
}
}
class Twin : Effect
{
public Twin(Effect nextEffect) : base(nextEffect) { }
public override void UseEffect()
{
Console.WriteLine("Twin Attack!");
if (nextEffect != null)
{
nextEffect.UseEffect();
}
}
}
Sword
// Sword 클래스
class Sword
{
private Effect effect;
public Sword()
{
this.effect = null;
}
public void Attack()
{
Console.WriteLine("Attack!");
if (effect != null)
{
effect.UseEffect();
}
}
public void AddEffect(Effect newEffect)
{
if (effect == null)
{
effect = newEffect;
}
else
{
// 기존 effect에 새로운 effect를 래핑하여 추가
Effect combinedEffect = newEffect;
combinedEffect.nextEffect = effect;
effect = combinedEffect;
}
}
}
Client
class Client
{
static void Main()
{
// Sword 객체 생성
Sword basicSword = new Sword();
// Fire, Bleed 효과 추가
basicSword.AddEffect(new Fire(new Bleed()));
//Twin 효과 추가
basicSword.AddEffect(new Twin());
// 공격 시 효과들이 실행됨
basicSword.Attack();
}
}
참고자료
GoF의 디자인 패턴 - 예스24
이 책은 디자인 패턴을 다룬 이론서로 디자인 패턴의 기초적이고 전반적인 내용을 학습할 수 있다.
www.yes24.com
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
11. [Design Pattern] 플라이웨이트(Flyweight) 패턴 (0) | 2023.12.27 |
---|---|
10. [Design Pattern] 퍼사드(Facade) 패턴 (0) | 2023.12.25 |
08. [Design Pattern] 컴포지트(Composite) 패턴 (1) | 2023.11.25 |
07. [Design Pattern] 브릿지(Bridge) 패턴 (0) | 2023.11.23 |
06. [Design Pattern] 어댑터(Adapter) 패턴 (0) | 2023.11.21 |