우선, GoF에서 이야기하는 디자인 패턴으로서의 Decorator는 Java, Python, Typescript 등의 언어에 등장하는 Decorator@
라는 기능과는 별개의 개념이다. 둘 다 상위 컨텐츠가 하위 컨텐츠를 감싸고 부가적인 기능을 제공하기에 같은 이름이 사용되지만, 다음의 두 가지 차이가 있다.
@
키워드와 함께 사용되는 언어 문법이다.우선 먼저 예제코드부터 살펴보자.
Decorator에는 네 가지 객체가 등장한다.
Decorator 패턴은 기존의 기능을 유지하면서 그 위에 새로운 기능을 덧대는 것이다. 그래서 아래처럼 처음에 주어진 문자열 주위에 장식을 이어붙여 나가는 예제를 만들기로 한다.
+-------------+
|Hello, world!|
+-------------+
먼저 객체들의 중심이 되며 공통 인터페이스를 담은 객체 Display
를 정의한다.
abstract class Display {
abstract getColumns(): number
abstract getRows(): number
abstract getRowText(row: number): string
show(): void {
for (let i = 0; i < this.getRows(); i++) {
console.log(this.getRowText(i))
}
}
}
타겟이되는 컨텐츠의 길이를 나타낼 수 있는 두 가지 메소드 getColumns
, getRows
와 컨텐츠의 각 줄을 출력하는 getRowText
의 인터페이스만을 정의하고 구현은 하위 클래스에 맡긴다.
그리고 내용물을 출력해주는 메소드 show
는 Template Method 패턴을 사용해 직접 구현한다.
내용물이 되는 구체적인 컴포넌트를 구현한다. 여기서는 장식의 대상이 되는 문자열을 담는 객체 StringDisplay
를 만든다.
class StringDisplay extends Display {
constructor(
private readonly string: string,
) {
super()
this.string = string
}
getColumns(): number {
return this.string.length
}
getRows(): number {
return 1
}
getRowText(row: number): string {
return row === 0 ? this.string : '';
}
}
다음으로 내용물을 꾸며줄 수 있는 Decorator의 인터페이스 Border
를 만든다. 이때, Border
도 여전히 Component이기 때문에 앞에서 정의한 Display
를 상속 받아야한다.
abstract class Border extends Display {
constructor(
protected readonly display: Display,
) {
super()
this.display = display
}
}
동시에 장식하는 대상이 되는 또 다른 Component는 위임을 받아온다.
마지막으로 Decorator Border
의 구현체를 만드는데, 내용물에 좌우 경계만 만들어주는 SideBorder
와, 위아래좌우 전부 감싸주는 FullBorder
두 가지 객체를 만든다.
class SideBorder extends Border {
constructor(
display: Display,
private readonly borderChar: string,
) {
super(display)
this.borderChar = borderChar
}
getColumns(): number {
return 1 + this.display.getColumns() + 1
}
getRows(): number {
return this.display.getRows()
}
getRowText(row: number): string {
return this.borderChar + this.display.getRowText(row) + this.borderChar
}
}
class FullBorder extends Border {
constructor(
display: Display,
) {
super(display)
}
getColumns(): number {
return 1 + this.display.getColumns() + 1
}
getRows(): number {
return 1 + this.display.getRows() + 1
}
getRowText(row: number): string {
if (row === 0) {
return '+' + this.makeLine('-', this.display.getColumns()) + '+'
} else if (row === this.display.getRows() + 1) {
return '+' + this.makeLine('-', this.display.getColumns()) + '+'
} else {
return '|' + this.display.getRowText(row - 1) + '|'
}
}
private makeLine(ch: string, count: number): string {
return ch.repeat(count);
}
}
먼저 작성한 코드에서 각 구현체의 역할을 확인해보자.
const b1: Display = new StringDisplay('Hello, world!');
const b2: Display = new SideBorder(b1, '#');
const b3: Display = new FullBorder(b2);
b1.show();
b2.show();
b3.show();
Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+
우선, b1
, b2
, b3
셋 다 Display
라는 공통된 타입으로 정의되어 있다. 하지만, 각각을 출력하게되면 b1
은 StringDisplay
를 생성하면서 입력한 문자열을 출력하고, b2
는 b1
에 #
을 양 옆에 붙여서 출력하며, 마지막으로 b3
는 b2
를 상자로 감싸서 출력한다.
이처럼 Decorator 객체들은 다른 Component에 덧대어서 부가적인 기능을 추가할 수 있으며, 아래 같은 내용의 작성도 가능하다.
const b4: Display = new SideBorder(
new FullBorder(
new FullBorder(
new SideBorder(
new FullBorder(
new StringDisplay('Hello, world.')
),
'*'
)
)
),
'/'
);
b4.show();
/+-------------------+/
/|+-----------------+|/
/||*+-------------+*||/
/||*|Hello, world.|*||/
/||*+-------------+*||/
/|+-----------------+|/
/+-------------------+/
여기서 다시한번 주목할 점은, Decorator 패턴을 이용해서 상위 객체를 위임받아 사용을하지만, 본래 상위 객체는 변형되지 않은채 그 위에 새 기능만이 추가된다는 것이다. 즉, Decorator를 아무리 덧붙여도 원래의 내용물이 가려지지 않는다는 것인데, 이런 특징을 투과적 인터페이스라고 부르기도 한다.
앞서서 다루었던 Composite 패턴도 마찬가지로 투과적 인터페이스의 예시이며, Composite과 Decorator 둘 다 동일한 내용물이 다른 내용물을 감싸는 재귀적인 형태의 구조가 등장한다. 하자만, 두 패턴의 목적이 다르다는 것에 주의해야한다. Composite은 내용물과 그릇을 동일한 컨텐츠로 취급하는 반면, Decorator 패턴은 기존 기능에 바깥 테두리를 추가해 나가는 것만이 주 목적이다.
Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)
Javascript MDN
Typescript Documentation