Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Usage in JavaScript: |
|
medium high |
The Decorator pattern extends (decorates) an object’s behavior dynamically. The ability to add new behavior at runtime is accomplished by a Decorator object which ‘wraps itself’ around the original object. Multiple decorators can add or override functionality to the original object.
An example of a decorator is security management where business objects are given additional access to privileged information depending on the privileges of the authenticated user. For example, an HR manager gets to work with an employee object that has appended (i.e. is decorated with) the employee's salary record so that salary information can be viewed.
Decorators provide flexibility to statically typed languages by allowing runtime changes as opposed to inheritance which takes place at compile time. JavaScript, however, is a dynamic language and the ability to extend an object at runtime is baked into the language itself.
For this reason, the Decorator pattern is less relevant to JavaScript developers. In JavaScript the Extend and Mixin patterns subsume the Decorator pattern. We will look at this in the JavaScript optimized code.
In the example code a User object is decorated (enhanced) by a DecoratedUser object. It extends the User with several address-based properties. The original interface must stay the same, which explains why user.name is assigned to this.name. Also, the say method of DecoratedUser hides the say method of User.
JavaScript itself is far more effective in dynamically extending objects with additional data and behavior. You can learn more about extending objects in the Modern Patterns under Mixin pattern. The JavaScript optimized solution below also demonstrates a better solution in JavaScript.
The log function is a helper which collects and displays results.
var User = function(name) { this.name = name; this.say = function() { log.add("User: " + this.name); }; } var DecoratedUser = function(user, street, city) { this.user = user; this.name = user.name; // ensures interface stays the same this.street = street; this.city = city; this.say = function() { log.add("Decorated User: " + this.name + ", " + this.street + ", " + this.city); }; } // logging helper var log = (function() { var log = ""; return { add: function(msg) { log += msg + "\n"; }, show: function() { alert(log); log = ""; } } })(); function run() { var user = new User("Kelly"); user.say(); var decorated = new DecoratedUser(user, "Broadway", "New York"); decorated.say(); log.show(); }Run
The Namespace pattern is applied to keep the code out of the global namespace. Our namespace is named Patterns.Classic. A Revealing Module named Decorator returns two methods: extend and extendDeep. They are described in the Modern Patterns section under the Mixin pattern (another 'decorator'). The extend method extends an object with additional properties and methods. extendDeep also extends an object but does it recursively which includes nested objects and arrays.
The Patterns object contains the namespace function which constructs namespaces non-destructively, that is, if a name already exists it won't overwrite it.
The log function is a helper which collects and displays results.
var Patterns = { namespace: function (name) { var parts = name.split("."); var ns = this; for (var i = 0, len = parts.length; i < len; i++) { ns[parts[i]] = ns[parts[i]] || {}; ns = ns[parts[i]]; } return ns; } }; Patterns.namespace("Classic").Decorator = (function () { var extend = function (dest, source) { for (var prop in source) { if (source.hasOwnProperty(prop)) { dest[prop] = source[prop]; } } }; var extendDeep = function (dest, source) { for (var prop in source) { if (source.hasOwnProperty(prop)) { if (typeof prop === "object") { dest[prop] = $.isArray(prop) ? [] : {}; this.deepExtend(dest[prop], source[prop]); } else { dest[prop] = source[prop]; } } } }; return { extend: extend, extendDeep: extendDeep }; })(); // log helper var log = (function () { var log = ""; return { add: function (msg) { log += msg + "\n"; }, show: function () { alert(log); log = ""; } } })(); function run() { var decorator = Patterns.Classic.Decorator; var User = function (name) { this.name = name; this.say = function () { log.add("User: " + this.name); }; } var user = new User("Kelly"); user.say(); decorator.extend(user, { street: "Broadway", city: "New York", say: function () { log.add("Extended User: " + this.name + ", " + this.street + ", " + this.city); } }); user.say(); decorator.extendDeep(user, { school: "Columbia", grades: { "Spring": 4.0, "Fall": 3.5 }, say: function () { log.add("Deeply Extended User: " + this.name + ", " + this.street + ", " + this.city + ", " + this.school + ", grades: " + this.grades.Spring + ", " + this.grades.Fall); } }); user.say(); log.show(); }Run