The Decorator Pattern
전통적으로 데코레이터는 시스템의 기존 클래스에 동작을 동적으로 추가 할 수있는 기능을 제공했습니다. 아이디어는 데코레이션 자체가 클래스의 기본 기능에 필수적이지 않다는 것입니다. 그렇지 않으면 슈퍼 클래스 자체로 구워집니다.
기존 코드를 사용하여 코드를 크게 수정할 필요없이 객체에 추가 기능을 추가하려는 기존 시스템을 수정하는 데 사용할 수 있습니다. 개발자가 사용하는 일반적인 이유는 응용 프로그램에 대량의 고유 한 유형의 객체를 필요로하는 기능이 포함되어 있기 때문입니다. JavaScript 게임을 위해 수백 가지의 다른 객체 생성자를 정의해야한다고 상상해보십시오.
객체 생성자는 서로 다른 기능을 가진 고유 한 플레이어 유형을 나타낼 수 있습니다. 반지의 제왕 게임에는 Hobbit
, Elf
, Orc
, Wizard
, Mountain Giant
, Stone Giant
등의 생성자가 필요할 수 있지만 수백 가지가 될 수 있습니다. 우리가 역량을 고려한다면 능력 유형 (예 : HobbitWithRing
, HobbitWithSword
, HobbitWithRingAndSword
등)의 각 조합에 대한 하위 클래스를 생성해야한다는 것을 상상해보십시오. 이는 매우 실용적이지 않으며 점점 더 많은 다른 능력.
데코레이터 패턴은 객체 생성 방법에 크게 의존하지 않고 기능 확장 문제에만 초점을 맞 춥니 다. 프로토 타입 상속에 의존하기보다는 단일 기본 객체로 작업하고 점진적으로 추가 기능을 제공하는 데코레이터 객체를 추가합니다. 아이디어는 하위 분류가 아닌 속성이나 메소드를 기본 객체에 추가 (장식)하여 좀 더 간소화한다는 것입니다.
자바 스크립트에서 객체에 새로운 속성을 추가하는 것은 매우 간단한 과정이므로 염두에두면 다음과 같이 매우 단순한 데코레이터를 구현할 수 있습니다.
예제 1 : 새로운 기능으로 생성자 장식하기
// A vehicle constructor
function Vehicle( vehicleType ){
// some sane defaults
this.vehicleType = vehicleType || "car";
this.model = "default";
this.license = "00000-000";
}
// Test instance for a basic vehicle
var testInstance = new Vehicle( "car" );
console.log( testInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
// Lets create a new instance of vehicle, to be decorated
var truck = new Vehicle( "truck" );
// New functionality we're decorating vehicle with
truck.setModel = function( modelName ){
this.model = modelName;
};
truck.setColor = function( color ){
this.color = color;
};
// Test the value setters and value assignment works correctly
truck.setModel( "CAT" );
truck.setColor( "blue" );
console.log( truck );
// Outputs:
// vehicle:truck, model:CAT, color: blue
// Demonstrate "vehicle" is still unaltered
var secondInstance = new Vehicle( "car" );
console.log( secondInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
이 단순한 구현의 유형은 기능적이지만 구현자가 제공해야하는 모든 장점을 실제로 보여주지는 못합니다. 이를 위해 먼저 Freeman, Sierra 및 Bates의 Head First Design Patterns라는 훌륭한 책에서 나온 Coffee 예제의 변형을 살펴볼 것입니다.이 책은 Macbook을 구입할 때 모델링됩니다.
예제 2 : 여러 장식자를 사용하여 객체 꾸미기
// The constructor to decorate
function MacBook() {
this.cost = function () { return 997; };
this.screenSize = function () { return 11.6; };
}
// Decorator 1
function memory( macbook ) {
var v = macbook.cost();
macbook.cost = function() {
return v + 75;
};
}
// Decorator 2
function engraving( macbook ){
var v = macbook.cost();
macbook.cost = function(){
return v + 200;
};
}
// Decorator 3
function insurance( macbook ){
var v = macbook.cost();
macbook.cost = function(){
return v + 250;
};
}
var mb = new MacBook();
memory( mb );
engraving( mb );
insurance( mb );
// Outputs: 1522
console.log( mb.cost() );
// Outputs: 11.6
console.log( mb.screenSize() );
위의 예제에서 우리의 Decorator는 MacBook()
슈퍼 클래스 객체 .cost()
함수를 재정 의하여 MacBook
의 현재 가격과 지정된 업그레이드 비용을 반환합니다.
이는 Macbook
의 일부로 정의 할 수있는 다른 속성뿐만 아니라 재정의 (예 : screenSize()
)되지 않은 원본 Macbook
객체 생성자 메서드로 장식을 변경하지 않고 그대로 유지하는 것으로 간주됩니다.
위의 예에서는 실제로 정의 된 인터페이스가 없으므로 작성자에서 수신자로 이동할 때 객체가 인터페이스를 충족하는지 확인해야합니다.
Pseudo-classical Decorators
이제 Dustin Diaz와 Ross Harmes의 Pro JavaScript Design Patterns (PJDP)에서 JavaScript 형식으로 처음으로 표시되는 데코레이터의 변형을 살펴 보겠습니다.
이전의 몇 가지 예제와 달리 Diaz와 Harmes는 "인터페이스"개념을 사용하여 다른 프로그래밍 언어 (예 : Java 또는 C ++)로 데코레이터를 구현하는 방법에 대해보다 자세히 설명합니다. 자세한 내용은 곧 자세히 설명합니다.
참고 : Decorator 패턴의 이러한 특정 변형은 참조 용으로 제공됩니다. 지나치게 복잡하면 이전에 다루었던 간단한 구현 중 하나를 선택하는 것이 좋습니다.
Interfaces
PJDP는 데코레이터를 동일한 인터페이스의 다른 객체 내부에있는 객체를 투명하게 래핑하는 데 사용되는 패턴으로 설명합니다. 인터페이스는 객체가 가져야하는 메소드를 정의하는 방법이지만, 실제로 메소드를 구현하는 방법을 직접 지정하지는 않습니다.
또한 매개 변수가 어떤 매개 변수를 사용하는지 나타낼 수도 있지만 선택 사항으로 간주됩니다.
그렇다면 JavaScript로 인터페이스를 사용하는 이유는 무엇입니까? 아이디어는 그들이 스스로 문서화하고 재사용을 촉진한다는 것입니다. 이론적으로 인터페이스는 코드를 구현하는 객체에 대한 변경을 보장함으로써 코드를 더욱 안정적으로 만듭니다.
아래는 오리 - 타이핑 (duck-typing)을 사용하는 JavaScript에서 인터페이스 구현의 예입니다.이 방법은 객체가 구현 된 메소드를 기반으로 생성자 / 객체의 인스턴스인지 여부를 판단하는 데 도움이됩니다.
// Create interfaces using a pre-defined Interface
// constructor that accepts an interface name and
// skeleton methods to expose.
// In our reminder example summary() and placeOrder()
// represent functionality the interface should
// support
var reminder = new Interface( "List", ["summary", "placeOrder"] );
var properties = {
name: "Remember to buy the milk",
date: "05/06/2016",
actions:{
summary: function (){
return "Remember to buy the milk, we are almost out!";
},
placeOrder: function (){
return "Ordering milk from your local grocery store";
}
}
};
// Now create a constructor implementing the above properties
// and methods
function Todo( config ){
// State the methods we expect to be supported
// as well as the Interface instance being checked
// against
Interface.ensureImplements( config.actions, reminder );
this.name = config.name;
this.methods = config.actions;
}
// Create a new instance of our Todo constructor
var todoItem = new Todo( properties );
// Finally test to make sure these function correctly
console.log( todoItem.methods.summary() );
console.log( todoItem.methods.placeOrder() );
// Outputs:
// Remember to buy the milk, we are almost out!
// Ordering milk from your local grocery store
위에서 Interface.ensureImplements
는 엄격한 기능 검사를 제공하며이 코드와 Interface
생성자는 여기에서 찾을 수 있습니다.
인터페이스의 가장 큰 문제점은 JavaScript에 내장 된 지원이 없기 때문에 이상적인 적합이 아닌 다른 언어의 기능을 모방하려고 시도 할 위험이 있다는 것입니다. 가벼운 인터페이스는 큰 성능 비용없이 사용할 수 있지만, 우리는이 동일한 개념을 사용하여 추상 장식자를 살펴볼 것입니다.
Abstract Decorators
이 버전의 Decorator 패턴의 구조를 보여주기 위해 MacBook을 다시 모델링하는 수퍼 클래스와 추가 비용으로 여러 가지 향상된 기능을 갖춘 MacBook
을 "장식"할 수있는 상점이 있다고 상상합니다.
강화 된 기능으로는 4GB 또는 8GB 램, 판화, 패러랠 또는 케이스로 업그레이드 할 수 있습니다. 이제 각 개선 옵션 조합에 대해 개별 하위 클래스를 사용하여이를 모델링한다면 다음과 같이 보일 수 있습니다.
var Macbook = function(){
//...
};
var MacbookWith4GBRam = function(){},
MacbookWith8GBRam = function(){},
MacbookWith4GBRamAndEngraving = function(){},
MacbookWith8GBRamAndEngraving = function(){},
MacbookWith8GBRamAndParallels = function(){},
MacbookWith4GBRamAndParallels = function(){},
MacbookWith8GBRamAndParallelsAndCase = function(){},
MacbookWith4GBRamAndParallelsAndCase = function(){},
MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){},
MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){};
등등.
이는 새로운 하위 클래스가 가능한 모든 개선 된 조합에 필요하기 때문에 비실용적 인 솔루션입니다. 큰 서브 클래스 집합을 유지하지 않고 간단하게 유지하기를 원할 때,이 문제를 더 잘 해결하기 위해 데코레이터를 어떻게 사용할 수 있는지 살펴 보겠습니다.
이전에 보았던 모든 조합을 요구하는 대신 5 개의 새로운 데코레이터 클래스를 만들어야합니다. 이러한 향상 클래스에서 호출되는 메소드는 Macbook
클래스로 전달됩니다.
다음 예제에서, 데코레이터는 컴포넌트를 투명하게 감싸고 있고, 같은 인터페이스를 사용하면서 흥미롭게도 상호 교환이 가능하다.
맥북에 대해 정의 할 인터페이스는 다음과 같습니다.
var Macbook = new Interface( "Macbook",
["addEngraving",
"addParallels",
"add4GBRam",
"add8GBRam",
"addCase"]);
// A Macbook Pro might thus be represented as follows:
var MacbookPro = function(){
// implements Macbook
};
MacbookPro.prototype = {
addEngraving: function(){
},
addParallels: function(){
},
add4GBRam: function(){
},
add8GBRam:function(){
},
addCase: function(){
},
getPrice: function(){
// Base price
return 900.00;
}
};
나중에 필요한만큼 더 많은 옵션을 추가하기 쉽게하기 위해, MacBook
인터페이스를 구현하는 데 필요한 기본 메소드로 Abstract Decorator 클래스가 정의됩니다. 나머지 클래스는 하위 클래스입니다. 추상 데코레이터는 가능한 모든 조합에 대해 클래스를 파생 할 필요없이 다양한 조합으로 필요한만큼의 데코레이터로 기본 클래스를 독립적으로 꾸밀 수 있습니다 (앞의 예를 기억하십시오).
// Macbook decorator abstract decorator class
var MacbookDecorator = function( macbook ){
Interface.ensureImplements( macbook, Macbook );
this.macbook = macbook;
};
MacbookDecorator.prototype = {
addEngraving: function(){
return this.macbook.addEngraving();
},
addParallels: function(){
return this.macbook.addParallels();
},
add4GBRam: function(){
return this.macbook.add4GBRam();
},
add8GBRam:function(){
return this.macbook.add8GBRam();
},
addCase: function(){
return this.macbook.addCase();
},
getPrice: function(){
return this.macbook.getPrice();
}
};
위 예제에서 MacBook
Decorator는 기본 구성 요소로 사용할 객체 (Macbook)를 허용합니다. 앞에서 정의한 Macbook
인터페이스를 사용하고 있으며 각 메소드마다 컴포넌트에서 동일한 메소드를 호출하고 있습니다. 이제 MacBook
Decorator를 사용하여 추가 할 수있는 옵션 클래스를 만들 수 있습니다.
// First, define a way to extend an object a
// with the properties in object b. We'll use
// this shortly!
function extend( a, b ){
for( var key in b )
if( b.hasOwnProperty(key) )
a[key] = b[key];
return a;
}
var CaseDecorator = function( macbook ){
this.macbook = macbook;
};
// Let's now extend (decorate) the CaseDecorator
// with a MacbookDecorator
extend( CaseDecorator, MacbookDecorator );
CaseDecorator.prototype.addCase = function(){
return this.macbook.addCase() + "Adding case to macbook";
};
CaseDecorator.prototype.getPrice = function(){
return this.macbook.getPrice() + 45.00;
};
여기에서 우리가하는 일은 장식해야 할 addCase()
와 getPrice()
메소드를 오버라이드하는 것입니다. 먼저 원래의 macbook
에서이 메소드를 호출 한 다음 단순히 문자열이나 숫자 값을 추가하면됩니다 (예 : 45.00) 그에 따라 그들에게.
지금까지이 섹션에서 많은 정보가 제공 되었으므로, 우리가 배운 것을 강조 할 수있는 한 가지 예를 들어 설명해 보도록하겠습니다.
// Instantiation of the macbook
var myMacbookPro = new MacbookPro();
// Outputs: 900.00
console.log( myMacbookPro.getPrice() );
// Decorate the macbook
var decoratedMacbookPro = new CaseDecorator( myMacbookPro );
// This will return 945.00
console.log( decoratedMacbookPro.getPrice() );
데코레이터는 객체를 동적으로 수정할 수 있으므로 기존 시스템을 변경하는 데 완벽한 패턴입니다. 경우에 따라 개체 주위에 데코레이터를 만드는 것보다 개체 유형마다 개별 하위 클래스를 유지하는 것이 더 간단 할 수도 있습니다. 이로 인해 많은 수의 하위 클래스 개체가 훨씬 더 직관적으로 필요할 수있는 응용 프로그램을 유지 관리 할 수 있습니다.
이 예제의 기능 버전은 JSBin에서 찾을 수 있습니다.
Decorators With jQuery
다른 패턴과 마찬가지로 jQuery로 구현할 수있는 Decorator 패턴의 예제도있다. jQuery.extend()
를 사용하면 두 개 이상의 객체 (및 해당 속성)를 런타임에 단일 객체로 확장 (또는 병합) 할 수 있습니다.
이 시나리오에서 대상 객체는 원본 / 수퍼 클래스 객체의 기존 메서드를 반드시 손상 시키거나 재정의하지 않고 새로운 기능으로 장식 할 수 있습니다 (이렇게 할 수 있음에도 불구하고).
다음 예제에서는 기본값, 옵션 및 설정의 세 가지 객체를 정의합니다. 이 작업의 목적은 optionssettings
에 있는 추가 기능을 사용하여 defaults
객체를 꾸미는 것입니다. 우리는 반드시:
(a) 나중에 발견되는 속성 또는 기능에 액세스 할 수있는 능력을 잃지 않는 "기본"상태로 둡니다. (b) "옵션"에있는 장식 된 속성 및 기능을 사용할 수있는 능력을 얻습니다.
var decoratorApp = decoratorApp || {};
// define the objects we're going to use
decoratorApp = {
defaults: {
validate: false,
limit: 5,
name: "foo",
welcome: function () {
console.log( "welcome!" );
}
},
options: {
validate: true,
name: "bar",
helloWorld: function () {
console.log( "hello world" );
}
},
settings: {},
printObj: function ( obj ) {
var arr = [],
next;
$.each( obj, function ( key, val ) {
next = key + ": ";
next += $.isPlainObject(val) ? printObj( val ) : val;
arr.push( next );
} );
return "{ " + arr.join(", ") + " }";
}
};
// merge defaults and options, without modifying defaults explicitly
decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp.options);
// what we have done here is decorated defaults in a way that provides
// access to the properties and functionality it has to offer (as well as
// that of the decorator "options"). defaults itself is left unchanged
$("#log")
.append( decoratorApp.printObj(decoratorApp.settings) +
+ decoratorApp.printObj(decoratorApp.options) +
+ decoratorApp.printObj(decoratorApp.defaults));
// settings -- { validate: true, limit: 5, name: bar, welcome: function (){ console.log( "welcome!" ); },
// helloWorld: function (){ console.log( "hello world" ); } }
// options -- { validate: true, name: bar, helloWorld: function (){ console.log( "hello world" ); } }
// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } }
장점 & 단점
개발자는이 패턴을 투명하게 사용할 수 있고 상당히 유연하기 때문에이 패턴을 사용하는 것을 즐긴다. 우리가 보았 듯이, 객체는 새로운 행동으로 포장되거나 꾸며질 수 있고 수정되는 기본 객체에 대해 걱정할 필요없이 계속 사용될 수있다. . 넓은 의미에서이 패턴은 또한 동일한 이점을 얻기 위해 많은 수의 서브 클래스에 의존 할 필요가 없다.
그러나 패턴을 구현할 때 우리가 알아야 할 단점이 있습니다. 관리가 잘 이루어지지 않으면 우리의 네임 스페이스에 많은 작지만 유사한 객체가 도입되므로 애플리케이션 아키텍처가 상당히 복잡해질 수 있습니다. 여기서 다루어야 할 점은 관리하기가 어려워 짐에 따라 패턴에 익숙하지 않은 다른 개발자는 왜 이것이 사용되는지를 파악하는 데 어려움을 겪을 수 있다는 것입니다.
충분 한 주석 달기 또는 패턴 연구가 후자를 지원해야하지만 우리 응용 프로그램에서 데코레이터를 얼마나 광범위하게 사용하는지에 대한 핸들을 유지하는 한 두 가지 모두에 대해 잘해야합니다.