객체에 동적으로 새로운 책임을 추가하는 패턴
상속을 통해 기능을 확장하는 대신, 객체를 감싸는 방식으로 기능을 추가하거나 변경
기능을 조합하여 새로운 객체를 생성 가능
서브클래스를 만드는 방식으로 행동을 상속받으면, 그 행동은 컴파일할 때 완전히 결정됨. 게다가 모든 서브클래스에서 똑같은 행동을 상속받아야함 => 구성을 통해 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정가능함
인터페이스나 추상 클래스로 정의된 기본 객체
객체에 동적으로 책임을 추가할 수 있어야 함
Component를 구현한 구체적인 클래스
기본 기능을 제공
Component와 동일한 인터페이스를 가지면서 내부에 Component를 갖는 클래스
자신의 행위를 수행한 후에 또 다른 Component를 호출하여 작업을 전달
Decorator를 구현한 구체적인 클래스
추가적인 기능을 제공하거나 기존 기능을 수정함
새로운 기능을 추가하거나 변경하기 쉬움. 상속보다 유연하게 기능을 확장 가능
객체를 감싸는 방식을 통해 기능을 조합하여 재사용 가능
class Coffee {
cost() {
return 5; // 기본 가격
}
description() {
return '기본 커피';
}
}
class CoffeeDecorator {
constructor(coffee) {
this._coffee = coffee;
}
cost() {
return this._coffee.cost();
}
description() {
return this._coffee.description();
}
}
class MilkDecorator extends CoffeeDecorator {
constructor(coffee) {
super(coffee);
}
cost() {
return super.cost() + 2; // 우유 추가 가격
}
description() {
return super.description() + ', 우유 추가';
}
}
class SugarDecorator extends CoffeeDecorator {
constructor(coffee) {
super(coffee);
}
cost() {
return super.cost() + 1; // 설탕 추가 가격
}
description() {
return super.description() + ', 설탕 추가';
}
}
// 기본 커피 주문
let simpleCoffee = new Coffee();
console.log(simpleCoffee.description()); // 기본 커피
console.log(simpleCoffee.cost()); // 5
// 우유 추가한 커피 주문
let milkCoffee = new MilkDecorator(simpleCoffee);
console.log(milkCoffee.description()); // 기본 커피, 우유 추가
console.log(milkCoffee.cost()); // 7
// 설탕 추가한 커피 주문
let sugarCoffee = new SugarDecorator(simpleCoffee);
console.log(sugarCoffee.description()); // 기본 커피, 설탕 추가
console.log(sugarCoffee.cost()); // 6
// 우유와 설탕 모두 추가한 커피 주문
let milkAndSugarCoffee = new SugarDecorator(new MilkDecorator(simpleCoffee));
console.log(milkAndSugarCoffee.description()); // 기본 커피, 우유 추가, 설탕 추가
console.log(milkAndSugarCoffee.cost()); // 8
클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀있어야한다는 디자인 원칙