Design Patterns in Ruby – Structural Patterns – Decorator Pattern
Intent
Decorator is a structural design pattern that allows you to attach new behaviors to objects by placing them inside special enclosing objects that contain those behaviors.
Problem
Imagine that you work on a notification library that allows other programs to notify their users about important events.
The original version of the Notifier
class library was based on only a few fields, constructors, and a single send
method. This method can accept a message argument from a client and send the message to a list of emails. The client is supposed to create and configure the notifier
object once, then use it whenever there’s a problem .
At some point, you realize that users of the library expect more than just email notifications. Many of them would like to receive an SMS about critical issues. Others would like to be notified on Facebook and, of course, the corporate users would love to get Slack notifications.
However you realize that library users expect more than just email notifications. Many of them want to receive an SMS about important issues. Others want to be notified on Facebook and of course, business users would love to receive notifications via Slack.
You extended the Notifier
class and placed additional notification methods on new subclasses. The customer is now obliged to initialize the desired message class and use it for all subsequent messages.
But then someone asks you, Why can you use several types of notifications at the same time? If your house is on fire, you may want to be notified via any channel.
You try to solve that problem by creating special subclasses that combine several notification methods in a class. However, it is clear that this approach will greatly expand the code, not only library code but also client code.
You have to find some other way to structure the message classes to avoid the number of subclasses accidentally breaking Guinness record. .
Solution
Extending a class is the first thing you think of when you need to change an object behavior. However, inheritance has some important things that you need to know.
- You cannot change the behavior of an existing object at run time. The object must be replaced with a new object created from the subclass.
- Child classes may have only one parent class. Most languages do not allow a class to inherit the behavior of multiple classes at the same time.
One of the ways to overcome these is to use Aggregation or Composition instead of Inheritance. Both schemes work almost the same way: one object has a reference to another object and delegates some work to it, while with inheritance, the object itself must do the job.
Wrapper is an alternative nickname for Decorator model expressing the main idea of the model. A “wrapper” is an object that can be associated with some “target” object. The wrapper contains the same set of methods as target and delegates the target to all requests it receives. However, the wrapper can change the result by doing something before or after it passes the request to the target.
When does a simple wrapper become a real decorator? As mentioned, the wrapper has the same interface as the wrapped object. That is why from the customer’s point of view, these objects are identical. Make the Wrapper accept any reference object with that interface. This will allow you to cover an object in multiple wrappers, adding the combined behavior of all wrappers to it.
In the example of the message, do the following:
The client code will need to wrap a basic wrapper object into a set of decorators that match the client’s options. The resulting object will be structured as a stack.
The last decorator in the stack will be the object the client actually works with. Because all decorators have the same interface as the base notifier, the rest of the client code doesn’t care whether it works with the notifier object or a decorated notifier object.
We can apply the same approach to other behaviors such as message formatting or compiling lists of recipients. Customers can decorate the object with any custom decorator, as long as they follow the same interface as the others.
Real-World Analogy
Dressing is an example of using decorators. When you are cold, you wrap yourself in a sweater. If you’re still cold with a sweater, you can wear a top coat. If it rains, you can wear a raincoat. All of these garments expand your basic behavior but are part of you, and you can easily remove any outfit whenever you don’t need it.
Structure
- The component declares a common interface for both
wrappers
andwrapped
objects. - Concrete Component is a class of
wrapped
objects. It defines basic behavior, which can be changed bydecorators
. - Base Decorator has a field to reference a
wrapped
object. Must be declared withcomponent interface
so that it can contain bothconcrete components
anddecorators
.base decorator
delegates all operations to thewrapped
object. - Concrete Decorators define additional behaviors that can be flexibly added to
components
.Concrete decorators
overridebase decorator
‘s methods and execute their behavior before or after calling the parent method. - The client can wrap
components
in multipledecorators
classes, as long as it works with all objects through thecomponent interface
.
Applicability
- Use the Decorator pattern when you need to be able to assign additional behaviors to objects at run time without changing the code using these objects.
- Use the model when it is difficult or impossible to extend an object behavior by inheritance.
How to Implement
- Make sure your business domain name can be represented as a key component with many optional classes on it.
- Indicates which methods are common to both the main component and optional classes. Create a
component interface
and declare those methods. - Create a
concrete component
class and define the basic behavior in it. - Create a
base decorator
class. It should have a field to store a reference to awrapped
object. The field must be declared as a component interface to allow binding toconcrete components
as well asdecorators
.base decorator
must delegate all the work for thewrapped
object. - Make sure all classes implement
component interface
. - Create
concrete decorators
by extendingbase decorator
. Aconcrete decorator
must perform its behavior before or after calling the parent method (always delegating to thewrapped
object). - The client code is responsible for creating
decorators
and composing them the way the client needs.
Decorator in Ruby
main.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | <span class="token comment"># base Component interface xác định các hoạt động có thể được thay đổi bởi các bộ điều khiển.</span> <span class="token keyword">class</span> <span class="token class-name">Component</span> <span class="token comment"># @return [String]</span> <span class="token keyword">def</span> operation <span class="token keyword">raise</span> <span class="token constant">NotImplementedError</span> <span class="token punctuation">,</span> <span class="token string">" <span class="token interpolation"><span class="token delimiter tag">#{</span> <span class="token keyword">self</span> <span class="token punctuation">.</span> <span class="token keyword">class</span> <span class="token delimiter tag">}</span></span> has not implemented method ' <span class="token interpolation"><span class="token delimiter tag">#{</span> __method__ <span class="token delimiter tag">}</span></span> '"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token comment"># Concrete Components cung cấp triển khai mặc định của các hoạt động. Có thể có một số biến thể của các lớp này.</span> <span class="token keyword">class</span> <span class="token class-name">ConcreteComponent</span> <span class="token operator"><</span> <span class="token constant">Component</span> <span class="token comment"># @return [String]</span> <span class="token keyword">def</span> operation <span class="token string">'ConcreteComponent'</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token comment"># Lớp base Decorator tuân theo cùng giao diện với các components khác.</span> <span class="token keyword">class</span> <span class="token class-name">Decorator</span> <span class="token operator"><</span> <span class="token constant">Component</span> attr_accessor <span class="token symbol">:component</span> <span class="token comment"># @param [Component] component</span> <span class="token keyword">def</span> <span class="token function">initialize</span> <span class="token punctuation">(</span> component <span class="token punctuation">)</span> <span class="token variable">@component</span> <span class="token operator">=</span> component <span class="token keyword">end</span> <span class="token comment"># The Decorator delegates all work to the wrapped component.</span> <span class="token keyword">def</span> operation <span class="token variable">@component</span> <span class="token punctuation">.</span> operation <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token comment"># Concrete Decorators gọi wrapped và thay đổi kết quả của nó theo một cách nào đó.</span> <span class="token keyword">class</span> <span class="token class-name">ConcreteDecoratorA</span> <span class="token operator"><</span> <span class="token constant">Decorator</span> <span class="token keyword">def</span> operation <span class="token string">"ConcreteDecoratorA( <span class="token interpolation"><span class="token delimiter tag">#{</span> @component <span class="token punctuation">.</span> operation <span class="token delimiter tag">}</span></span> )"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token comment"># Decorators có thể thực hiện hành vi của họ trước hoặc sau khi gọi đến một đối tượng wrapped.</span> <span class="token keyword">class</span> <span class="token class-name">ConcreteDecoratorB</span> <span class="token operator"><</span> <span class="token constant">Decorator</span> <span class="token comment"># @return [String]</span> <span class="token keyword">def</span> operation <span class="token string">"ConcreteDecoratorB( <span class="token interpolation"><span class="token delimiter tag">#{</span> @component <span class="token punctuation">.</span> operation <span class="token delimiter tag">}</span></span> )"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token comment"># Mã client hoạt động với tất cả các đối tượng sử dụng Component interface. Bằng cách này, nó có thể độc lập với các lớp cụ thể của các components mà nó làm việc với.</span> <span class="token keyword">def</span> <span class="token function">client_code</span> <span class="token punctuation">(</span> component <span class="token punctuation">)</span> <span class="token comment"># ...</span> print <span class="token string">"RESULT: <span class="token interpolation"><span class="token delimiter tag">#{</span> component <span class="token punctuation">.</span> operation <span class="token delimiter tag">}</span></span> "</span> <span class="token comment"># ...</span> <span class="token keyword">end</span> simple <span class="token operator">=</span> <span class="token constant">ConcreteComponent</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token class-name">puts</span> <span class="token string">'Client: I've got a simple component:'</span> <span class="token function">client_code</span> <span class="token punctuation">(</span> simple <span class="token punctuation">)</span> puts <span class="token string">"nn"</span> <span class="token comment"># Lưu ý cách decorators có thể bao bọc không chỉ các decoratorsn đơn giản mà cả các decorators khác.</span> decorator1 <span class="token operator">=</span> <span class="token constant">ConcreteDecoratorA</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> simple <span class="token punctuation">)</span> decorators decorator2 <span class="token operator">=</span> <span class="token constant">ConcreteDecoratorB</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> decorator1 <span class="token punctuation">)</span> puts <span class="token string">'Client: Now I've got a decorated component:'</span> <span class="token function">client_code</span> <span class="token punctuation">(</span> decorator2 <span class="token punctuation">)</span> |
output.txt:
1 2 3 4 5 6 | Client: I've got a simple component: RESULT: ConcreteComponent Client: Now I've got a decorated component: RESULT: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent)) |