본문으로 건너뛰기

데코레이터 패턴_JavaScript 디자인 패턴 11

무료2015-08-12#JS#Design_Pattern#JavaScript装饰者模式#伪经典装饰者#抽象装饰者

데코레이터 패턴의 핵심은 "래핑" 으로, 여러 개의 선택 가능한 데코레이터 타입을 제공하고, 초기 타입 오브젝트에 필요한 데코레이터 타입을 래핑함으로써 초기 타입의 기능을 확장하여 타입의 대폭발을 회피한다. 본고는 JS 로 구현된 데코레이터 (Decorator) 패턴을 상세히 소개

##一.가장 심플한 데코레이터 구현

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();

심플함은 충분히 심플하지만, 몇 가지 문제가 존재합니다:

  1. 데코레이터 패턴을 사용할 필요가 없는 것 아닌가? 기능을 모두 Dog 에 넣으면 되지 않는가?

위의 예에서 보면 확실히 그렇지만, 만약 Dog 가 직접 수정할 수 없는 제 3 자 컴포넌트라면, 이때 데코레이터 패턴을 통해 기능을 확장하는 것은 매우 적합합니다. 이 각도에서 보면, 데코레이터 패턴과 퍼사드 패턴은 매우 비슷하며, 유일한 차이는 목적이 다른 것으로, 전자는 신기능을 확장하기 위한 것이고, 후자는 기존 인터페이스의 사용 편의성을 추구합니다

  1. 어떤 기능을 데코레이터 타입으로 존재시켜야 하는가?

기초적, 필수적인 기능은 Dog 의 구성 부분이어야 하며, 선택 가능, 추가적, 자주 사용되지 않는 기능은 데코레이터 타입이 제공해야 합니다

  1. 데코레이터가 부주의로 기존의 속성을 덮어쓰면 어떻게 하는가?

확실히 속성이 덮어씌워질 리스크가 존재합니다. 왜냐하면 우리는 타입상의 아무런 제약도 하지 않았고, 각 데코레이터 간도 상대적으로 독립적이며, 다른 데코레이터가 추가한 속성을 커버해버릴 가능성도 있기 때문입니다. 우리는 더 신뢰할 수 있는 (후문에서 소개하는) 데코레이터 구현으로 이러한 리스크를 회피해야 합니다

##二.의사 클래식 데코레이터

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 패턴의 구현을 제공한다고 말하고, 여기서 또 데코레이터 패턴의 구현을 제공한다고 말하지만, 모순되지 않습니다. 왜냐하면 이 두 패턴의 목적은 기존 컴포넌트의 기능을 확장하는 것으로, 엄밀히 말하면, jQueryextend 는 더 Mixin 패턴과 비슷합니다 (몇 개의 컴포넌트를 합병하여 신컴포넌트를 생성하며, 만약 이 메커니즘이 데코레이터 패턴이라고 계산한다면, 억지로说得过去합니다)

jQueryextend 에 관한 더 많은 정보는 아래를 참조: jQuery.extend 함수 상세

##五.데코레이터 패턴의 우열점

장점

  1. 기초 기능과 선택 가능 기능 (데코레이터 기능) 을 분리

  2. 오브젝트의 기능을 동적으로 확장할 수 있으며, 기본 오브젝트를 의외로 수정하지는 않음

단점

  1. 대량의 소타입을 도입하여, 네임스페이스의 혼란을 일으킬 수 있음

  2. 관리가 부적절하면 애플리케이션의 구조를 더 복잡하게 하며, 특히 상속 메커니즘에 기반한 구현에서는, 심층 상속은 가독성을极大地로 저하시킴

참고 자료

  • 『JavaScript 디자인 패턴』

댓글

아직 댓글이 없습니다

댓글 작성