##一.가장 심플한 데코레이터 구현
JS 는 동적 확장方面에서 천생의 우위를 가지며, 쉽게 데코레이터를 구현할 수 있습니다:
// 초기 타입
function Dog() {
console.log('I am a dog');
}
// 데코레이터 타입
function CanRun(dog) {
dog.run = function() {
console.log('I can run');
}
return dog;
}
function CanWalk(dog) {
dog.walk = function() {
console.log('I can walk');
}
return dog;
}
function CanBark(dog) {
dog.bark = function() {
console.log('I can bark');
}
return dog;
}
// ...
// test
var dog = new Dog();
CanWalk(CanBark(CanRun(dog))); // "래핑" 을 통해 dog 의 기능 확장
dog.run();
dog.bark();
dog.walk();
심플함은 충분히 심플하지만, 몇 가지 문제가 존재합니다:
- 데코레이터 패턴을 사용할 필요가 없는 것 아닌가? 기능을 모두
Dog에 넣으면 되지 않는가?
위의 예에서 보면 확실히 그렇지만, 만약 Dog 가 직접 수정할 수 없는 제 3 자 컴포넌트라면, 이때 데코레이터 패턴을 통해 기능을 확장하는 것은 매우 적합합니다. 이 각도에서 보면, 데코레이터 패턴과 퍼사드 패턴은 매우 비슷하며, 유일한 차이는 목적이 다른 것으로, 전자는 신기능을 확장하기 위한 것이고, 후자는 기존 인터페이스의 사용 편의성을 추구합니다
- 어떤 기능을 데코레이터 타입으로 존재시켜야 하는가?
기초적, 필수적인 기능은 Dog 의 구성 부분이어야 하며, 선택 가능, 추가적, 자주 사용되지 않는 기능은 데코레이터 타입이 제공해야 합니다
- 데코레이터가 부주의로 기존의 속성을 덮어쓰면 어떻게 하는가?
확실히 속성이 덮어씌워질 리스크가 존재합니다. 왜냐하면 우리는 타입상의 아무런 제약도 하지 않았고, 각 데코레이터 간도 상대적으로 독립적이며, 다른 데코레이터가 추가한 속성을 커버해버릴 가능성도 있기 때문입니다. 우리는 더 신뢰할 수 있는 (후문에서 소개하는) 데코레이터 구현으로 이러한 리스크를 회피해야 합니다
##二.의사 클래식 데코레이터
JS 는 Interface 서포트를 제공하지 않아, 인터페이스를 통해 타입을 제약하여 그 신뢰성을 높일 수 없지만, 스스로 Interface 를 구현하여 타입을 제약할 수 있습니다. 심플한 Interface 는 이럴 수 있습니다:
function Interface(strName, arrMethodNames) {
this.name = strName;
this.strMethods = arrMethodNames;
}
Interface.ensureImplements = function(obj, interface) {
for(var i = 0; i < interface.strMethods.length; i++) {
if (!(interface.strMethods[i] in obj)) {
throw new TypeError('Interface.ensureImplements: no ' + interface.strMethods[i] + '\'s here');
}
}
}
커스텀의 Interface 를 이용하여 타입 제약을 실현하고, 데코레이터 패턴은 이럴 수 있습니다:
// 데코레이터 오브젝트에 상당하는 작용
var spec = {
attr: 'value',
actions: {
fun1: function() {
console.log('fun1');
},
fun2: function() {
console.log('fun2');
}
}
}
var myInterface = new Interface('myInterface', ['fun1', 'fun2']);
// 생성자
function MyObject(spec) {
// 인터페이스 검사
Interface.ensureImplements(spec.actions, myInterface);
this.attr = spec.attr;
this.methods = spec.actions;
}
// test
var obj = new MyObject(spec);
obj.methods.fun1();
obj.methods.fun2();
인터페이스를 이용하여 타입 제약을 실현했지만, 구조가 명확하지 않고, 관리에 불편합니다. 가장 관리하기 쉬운 것은 물론 계층 구조로, 즉 아래의 추상 데코레이터 중의 상속 메커니즘입니다
##三.추상 데코레이터
선택 가능 기능을 먼저 추상 데코레이터 클래스 중에 정의하지만, 구현을 제공하지 않고, 구체 데코레이터 서브클래스가 구현을 제공하며, 인터페이스를 이용하여 타입 제약을 실현합니다. 샘플 코드는 다음과 같습니다:
// 인터페이스 정의
var iCoffee = new Interface('coffee', ['addMilk', 'addSalt', 'addSugar']);
// 기본 클래스 정의
function Coffee() {
console.log('make a cup of coffee');
}
Coffee.prototype = {
addMilk: function() {},
addSalt: function() {},
addSugar: function() {},
getPrice: function() {
// 原味 가격
return 30;
}
}
// 추상 데코레이터 클래스 정의
function CoffeeDecorator(coffee) {
Interface.ensureImplements(coffee, iCoffee);
this.coffee = coffee;
}
CoffeeDecorator.prototype = {
addMilk: function() {
return this.coffee.addMilk();
},
addSalt: function() {
return this.coffee.addSalt();
},
addSugar: function() {
return this.coffee.addSugar();
},
getPrice: function() {
return this.coffee.getPrice();
}
}
// 데코레이터 서브클래스
function MilkDecorator(coffee) {
// 부모 클래스 생성자 호출
this.superType(coffee);
}
// 상속 정의
function extend(subType, superType) {
var F = function() {};
F.prototype = superType.prototype;
subType.prototype = new F(); // 프로토타입 속성 상속
subType.prototype.superType = superType;
console.log(subType.prototype.superType);
}
extend(MilkDecorator, CoffeeDecorator); // 상속
// 부모 클래스 메서드 재작성 (확장)
MilkDecorator.prototype.addMilk = function() {
console.log('add some milk');
}
MilkDecorator.prototype.getPrice = function() {
return this.coffee.getPrice() + 8;
}
// ...다른 데코레이터 서브클래스 정의
// test
var coffee = new Coffee();
console.log(coffee.getPrice()); // 30
coffee = new MilkDecorator(coffee);
console.log(coffee.getPrice()); // 38
이 구현 방식의 장점은 구조가 명확한 것이고, 단점은 복잡도가 증가한 것입니다. 언어 자체가 제공하지 않는 특성을 수동으로 시뮬레이션하는 것은 잠재적인 리스크가 있을 수 있으며, 우리는 인터페이스와 상속 메커니즘을 시뮬레이션하여 다른 숨은 위험을 묻을 수 있습니다
##四.jQuery 가 제공하는 데코레이터 메커니즘
네, 또 $.extend() 입니다. [Mixin 패턴_JavaScript 디자인 패턴 10](/articles/mixin 패턴-javascript 디자인 패턴 10/) 에서 $.extend() 가 Mixin 패턴의 구현을 제공한다고 말하고, 여기서 또 데코레이터 패턴의 구현을 제공한다고 말하지만, 모순되지 않습니다. 왜냐하면 이 두 패턴의 목적은 기존 컴포넌트의 기능을 확장하는 것으로, 엄밀히 말하면, jQuery 의 extend 는 더 Mixin 패턴과 비슷합니다 (몇 개의 컴포넌트를 합병하여 신컴포넌트를 생성하며, 만약 이 메커니즘이 데코레이터 패턴이라고 계산한다면, 억지로说得过去합니다)
jQuery 의 extend 에 관한 더 많은 정보는 아래를 참조: jQuery.extend 함수 상세
##五.데코레이터 패턴의 우열점
장점
-
기초 기능과 선택 가능 기능 (데코레이터 기능) 을 분리
-
오브젝트의 기능을 동적으로 확장할 수 있으며, 기본 오브젝트를 의외로 수정하지는 않음
단점
-
대량의 소타입을 도입하여, 네임스페이스의 혼란을 일으킬 수 있음
-
관리가 부적절하면 애플리케이션의 구조를 더 복잡하게 하며, 특히 상속 메커니즘에 기반한 구현에서는, 심층 상속은 가독성을极大地로 저하시킴
참고 자료
- 『JavaScript 디자인 패턴』
아직 댓글이 없습니다