Weâre now going to examine a variation of the Decorator first presented in a JavaScript form in Pro JavaScript Design Patterns (PJDP) by Dustin Diaz and Ross Harmes.
Unlike some of the examples from earlier, Diaz and Harmes stick more closely to how decorators are implemented in other programming languages (such as Java or C++) using the concept of an âinterface,â which we will define in more detail shortly.
Note
This particular variation of the Decorator pattern is provided for reference purposes. If you find it overly complex, I recommend opting for one of the simpler implementations covered earlier.
PJDP describes the Decorator pattern as one that is used to transparently wrap objects inside other objects of the same interface. An interface is a way of defining the methods an object should have; however, it doesnât actually directly specify how those methods should be implemented.
Interfaces can also indicate what parameters the methods take, but this is considered optional.
So, why would we use an interface in JavaScript? The idea is that theyâre self-documenting and promote reusability. In theory, interfaces also make code more stable by ensuring changes to them must also be made to the objects implementing them.
Below is an example of an implementation of interfaces in JavaScript using duck-typing, an approach that helps determine whether an object is an instance of that constructor/object based on the methods it implements.
// 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
=
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
In this example, Interface.ensureImplements
provides strict
functionality checking, and code for both this and the Interface
constructor can be found here.
The biggest problem with interfaces is that, as there isnât built-in support for them in JavaScript, there is a danger of attempting to emulate a feature of another language that may not be an ideal fit. Lightweight interfaces can be used without a great performance cost, however, and we will next look at Abstract Decorators using this same concept.
To demonstrate the structure of this version of the
Decorator pattern, weâre going to imagine we have a superclass that
models a Macbook
once again and a
store that allows us to âdecorateâ our Macbook with a number of
enhancements for an additional fee.
Enhancements can include upgrades to 4 GB or 8 GB of RAM, engraving, Parallels, or a case. Now if we were to model this using an individual subclass for each combination of enhancement options, it might look something like this:
var
Macbook
=
function
(){
//...
};
var
MacbookWith4GBRam
=
function
(){},
MacbookWith8GBRam
=
function
(){},
MacbookWith4GBRamAndEngraving
=
function
(){},
MacbookWith8GBRamAndEngraving
=
function
(){},
MacbookWith8GBRamAndParallels
=
function
(){},
MacbookWith4GBRamAndParallels
=
function
(){},
MacbookWith8GBRamAndParallelsAndCase
=
function
(){},
MacbookWith4GBRamAndParallelsAndCase
=
function
(){},
MacbookWith8GBRamAndParallelsAndCaseAndInsurance
=
function
(){},
MacbookWith4GBRamAndParallelsAndCaseAndInsurance
=
function
(){};
⦠and so on.
This would be an impractical solution, as a new subclass would be required for every possible combination of enhancements that are available. As we would prefer to keep things simple without maintaining a large set of subclasses, letâs look at how decorators may be used to solve this problem better.
Rather than requiring all of the combinations we saw earlier, we
should simply have to create five new decorator classes. Methods that
are called on these enhancement classes would be passed on to our
Macbook
class.
In our next example, decorators transparently wrap around their components and can interestingly be interchanged as they use the same interface.
Hereâs the interface weâre going to define for the 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
;
}
};
To make it easier for us to add as many more options as needed
later on, an Abstract Decorator class is defined with default methods
required to implement the Macbook
interface, which the rest of the options will subclass. Abstract
Decorators ensure that we can decorate a base class independently with
as many decorators as needed in different combinations (remember the
example earlier?) without needing to derive a class for every possible
combination.
// 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
();
}
};
Whatâs happening in the above sample is that the Macbook
Decorator is taking an object to use
as the component. Itâs using the Macbook
interface we defined earlier, and for
each method, it calls the same method on the component. We can now
create our option classes just by using the Macbook
Decorator; simply call the superclass
constructor, and any methods can be overridden as necessary.
var
CaseDecorator
=
function
(
macbook
){
// call the superclass's constructor next
this
.
superclass
.
constructor
(
macbook
);
};
// Let's now extend the superclass
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
;
};
As we can see, most of this is relatively straightforward to
implement. What weâre doing is overriding the addCase()
and getPrice()
methods that need to be decorated,
and weâre achieving this by first executing the componentâs method and
then adding to it.
As thereâs been quite a lot of information presented in this section so far; letâs try to bring it all together in a single example that will hopefully highlight what we have learned.
// Instantiation of the macbook
var
myMacbookPro
=
new
MacbookPro
();
// Outputs: 900.00
console
.
log
(
myMacbookPro
.
getPrice
()
);
// Decorate the macbook
myMacbookPro
=
new
CaseDecorator
(
myMacbookPro
);
// This will return 945.00
console
.
log
(
myMacbookPro
.
getPrice
()
);
As decorators are able to modify objects dynamically, theyâre a perfect pattern for changing existing systems. Occasionally, itâs just simpler to create decorators around an object versus the trouble of maintaining individual subclasses for each object type. This makes maintaining applications that may require a large number of subclassed objects significantly more straightforward.
Get Learning JavaScript Design Patterns now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.