Decorator
- lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
Definition
- original definition
- Attach additional responsibilities to an object dynamically
- decorators provide a flexible alternative to sub classing for extending functionality
- a good way to define pluggable behaviors and features for clients that do not alter their original function but add extra abilities
- also known as a wrapper pattern
- when used with functions, a decorator ingests a function and returns back a function
- this way it behaves as a higher order function
- when used with functions, a decorator ingests a function and returns back a function
- example
- the react framework uses the concept of higher order components
- which results in a component gaining extra abilities and functionality
- the react framework uses the concept of higher order components
Problem
- In a notification library which lets other programs notify their users about important events
- the initial version of the library was based on the Notifier class that had a few fields, a constructor and a single send method
- the method could accept a message argument from a client
- and send the message to a list of emails that were passed to the notifier via its constructor
- a 3rd party app which acted as a client was supposed to create and configure the notifier object once
- and then use it each time something important happened
- then you realize that users of the library expect more than just email notifications
- many of them want to receive SMS about critical issues
- others want to be notified on Facebook
- corporate users want to get Slack notifications
- if you try to extend the Notifier class
- and put the additional notification methods into new subclasses
- now the client needs to instantiate the desire notification class
- and use it for all further notifications
- and put the additional notification methods into new subclasses
- but if someone asked you to use several notification types at once
- can try to address that problem by creating special subclasses
- which combined several notification methods within 1 class
- however, this approach would bloat the code immensely
- for both the library code and the client code
- can try to address that problem by creating special subclasses
- thus a need to find some other way to structure notifications classes so that their number won't accidentally break some record
Solution
- extending a class is the 1st thing that comes to mind when needed to alter an object's behavior
- however, inheritance has several serious caveats that we need to be aware of
- inheritance is static
- we can't alter behavior of an existing object at runtime
- can only replace the whole object with another 1 that's created from a different subclass
- subclasses can have just 1 parent class
- in most languages, inheritance doesn't let a class inherit behaviors of multiple classes at the same time
- inheritance is static
- however, inheritance has several serious caveats that we need to be aware of
- To overcome these caveats is by using Aggregation or Composition instead of inheritance
- both alternatives work almost the same way
- 1 object has a reference to another and delegates it some work
- whereas with inheritance, the object itself is able to do that work
- inheriting the behavior from its superclass
- both alternatives work almost the same way
- with this new approach, can easily substitute the linked helper object with another
- changing the behavior of the container at runtime
- an object can use the behavior of various classes
- having references to multiple objects and delegating them all kinds of work
- aggregation / composition is the key principle behind many design patterns including decorator
- Wrapper is the alternative nickname for the Decorator pattern that clearly expresses the main idea of the pattern
- a wrapper is an object that can be linked with some target object
- the wrapper contains the same set of methods as the target and delegates to it all requests it received
- however, the wrapper may alter the result by doing something either before or after it passes the request to the target
- when does the wrapper become a real decorator
- the wrapper implements the same interface as the wrapped object
- from the client's perspective, these objects are identical
- make the wrapper's reference field accept any object that follows that interface
- this will let you cover an object in multiple wrappers, adding the combined behavior of all the wrappers to it
- the wrapper implements the same interface as the wrapped object
- in the notification example, leave the simple email notification behavior inside the base Notifier class
- but turn all other notification methods into decorators
- the client code would need to wrap a basic notifier object into a set of decorators that match the client's preferences
- the resulting objects will be structures as a stack
- the last decorator in the stack would be the object that the client actually works with
- since all decorators implement the same interface as the base notifier
- the rest of the client code won't care whether it works with the pure notifier object or the decorated one
- the resulting objects will be structures as a stack
- the same approach could be applied to other behaviors such as formatting messages or composing the recipient list
- the client can decorate the object with any custom decorates
- as long as they follow the same interface as the others
- the client can decorate the object with any custom decorates
Analogy
- wearing clothes is an example of decorators
- when you're cold, you wrap yourself in a sweater
- if you're cold with a sweater, you can wear a jacket on top
- if it's raining, you can put on a raincoat
- all of these garments extend your basic behavior but aren't part of you
- you can easily take off any piece of clothing whenever you don't need it
Structure
The Component declares the common interface for both wrappers and wrapped objects.
Concrete Component is a class of objects being wrapped.
- It defines the basic behavior, which can be altered by decorators.
The Base Decorator class has a field for referencing a wrapped object.
- The field’s type should be declared as the component interface so it can contain both concrete components and decorators.
- The base decorator delegates all operations to the wrapped object.
Concrete Decorators define extra behaviors that can be added to components dynamically.
- Concrete decorators override methods of the base decorator and execute their behavior either before or after calling the parent method.
The Client can wrap components in multiple layers of decorators, as long as it works with all objects via the component interface.
When to use
Use the Decorator pattern when you need to be able to assign extra behaviors to objects at runtime without breaking the code that uses these objects
- The Decorator lets you structure your business logic into layers,
- create a decorator for each layer
- and compose objects with various combinations of this logic at runtime
- The client code can treat all these objects in the same way, since they all follow a common interface
- The Decorator lets you structure your business logic into layers,
Use the pattern when it’s awkward or not possible to extend an object’s behavior using inheritance
- Many programming languages have the
final
keyword that can be used to prevent further extension of a class- For a final class, the only way to reuse the existing behavior would be to wrap the class with your own wrapper, using the Decorator pattern
- Many programming languages have the
How to implement
- Make sure your business domain can be represented as a primary component with multiple optional layers over it
- Figure out what methods are common to both the primary component and the optional layers
- Create a component interface and declare those methods there
- Create a concrete component class and define the base behavior in it
- Create a base decorator class
- It should have a field for storing a reference to a wrapped object
- The field should be declared with the component interface type to allow linking to concrete components as well as decorators
- The base decorator must delegate all work to the wrapped object
- Make sure all classes implement the component interface
- Create concrete decorators by extending them from the base decorator
- A concrete decorator must execute its behavior before or after the call to the parent method (which always delegates to the wrapped object)
- The client code must be responsible for creating decorators and composing them in the way the client needs
Pros & Cons
Pros
- can extend an object’s behavior without making a new subclass
- can add or remove responsibilities from an object at runtime
- can combine several behaviors by wrapping an object into multiple decorators
- Single Responsibility Principle
- can divide a monolithic class that implements many possible variants of behavior into several smaller classes
Cons
- It’s hard to remove a specific wrapper from the wrappers stack
- It’s hard to implement a decorator in such a way that its behavior doesn’t depend on the order in the decorators stack
- The initial configuration code of layers might look pretty ugly
Summary
- the decorator pattern, ingests a function and returns back a function
- decorators can be used to add features and function to existing objects dynamically
- implemented as higher order functions
Example
Typescript
interface Coffee {
getCost();
getDescription();
}
class SimpleCoffee implements Coffee {
public getCost() {
return 10;
}
public getDescription() {
return "Simple coffee";
}
}
class CoffeeDecorator implements Coffee {
protected wrappee: Coffee;
constructor(coffee: Coffee) {
this.wrappee = coffee;
}
public getCost() {
return this.wrappee.getCost();
}
public getDescription() {
return this.wrappee.getDescription();
}
}
class MilkCoffeeDecorator extends CoffeeDecorator {
public getCost(): number {
return this.wrappee.getCost() + 2;
}
public getDescription(): string {
return this.wrappee.getDescription() + "with milk";
}
}
class WhipCoffeeDecorator extends CoffeeDecorator {
public getCost(): number {
return this.wrappee.getCost() + 3;
}
public getDescription(): string {
return this.wrappee.getDescription() + "and with whip";
}
}
let someCoffee = new SimpleCoffee();
someCoffee.getCost(); // 10
someCoffee.getDescription(); // Simple Coffee
someCoffee = new MilkCoffeeDecorator(someCoffee);
someCoffee.getCost(); // 12
someCoffee = new WhipCoffeeDecorator(someCoffee);
someCoffee.getDescription(); // Simple Coffee with milk and whip
Javascript
- the TC39 has proposed the decorator (@decorator) syntax for use with classes and class methods
- until the format is released, the decorator syntax can be implemented using the babel complier
Python
- the decorator design pattern is NOT the same as the python decorator / function wrapper