Gang Of Four - Ruby Decorator
Aug 12, 2017
A Decorator is a design pattern. It Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
module Milk
def cost
super + 0.4
end
end
module Sugar
def cost
super + 0.2
end
end
class Coffee
def cost
2
end
end
java = Coffee.new
java.extend(Milk) # => 2.4
java.extend(Sugar) # => 2.6
java.cost # 2.6
This magical 'extend' looks bit intimidating. Plus the argument it passes are module (Milk and Sugar), not a class. and super keywords are in the module, again, not in class. It's bit unusual case but you can see how elegantly it attaches responsibilities to class from those modules. Interesting part is also when it calls `extend(Sugar)` it does not only returns 2 + 0.2, it actually added up Milk price as well. That's because super and it gets whatever its value and then added 0.2.
Include vs Extend
Include is a Mixin. When a class `includes` a module (and that's always the case, you don't mixin a class and it throws TypeError), that module's instance methods become available as instance methods of the class. It's almost as if the module becomes a superclass of the class that uses it. Great thing about including module is that you can include many different classes and inheritance chain. If multiple modules are included, they are added to the chain in order.
If a module itself includes other modules, a chain of proxy classes will be added to any class that includes that module, one proxy for each module that is directly or indirectly included.
module Cream
def fat
puts "very fatty"
end
end
module Milk
include Cream
def cost
0.4
end
end
module Cream
def cost
0.9
end
end
class Coffee
# include can be chained in order
include Milk
include Cream
def cost
super + 0.2
end
end
Coffee.new.cost # => 1.1
Coffee.new.fat # => "very fatty"
Extending object isn't also much different. You can mix a module into an object using Object#extend. There is an interesting trick with extend. If you use it within a class definition, the module's methods become class methods. Also extend doesn't care left side of argument. Even string can use method if it is extended. But it is only allow to module to be extended.
module Cream
def fat
puts "very fatty"
end
end
module Milk
def cost
0.4
end
end
class Coffee
def cost
super + 2
end
end
Coffee.new.extend(Sugar).cost # => wrong argument type Class (expected Module) (TypeError)
Coffee.extend(Cream).fat # => very fatty
a = "hello"
a.extend(Sugar).cost # => 0.4
a.extend(Coffee).cost # => TypeError 'Coffee is not module'
Alternative Decorator - Plain Old Ruby Object Decorator
Ruby allows to passing responsibilities by passing objects around as an argument using initialize.
class Coffee
def cost
2
end
def origin
"Colombia"
end
end
class Milk
def initialize(component)
@component = component
end
def cost
@component.cost + 0.4
end
end
coffee = Coffee.new
Sugar.new(Milk.new(coffee)).cost # 2.6
Sugar.new(Sugar.new(coffee)).cost # 2.4
Sugar.new(Milk.new(coffee)).class # Sugar
Milk.new(coffee).origin # NoMethodError
Alternative Decorator - 'Method Missing' decorator
module Decorator
def initialize(component)
@component = component
end
def method_missing(meth, *args)
if @component.respond_to?(meth)
@component.send(meth, *args)
else
super
end
end
def respond_to?(meth)
@component.respond_to?(meth)
end
end
class Coffee
def cost
2
end
def origin
"Colombia"
end
end
class Milk
include Decorator
def cost
@component.cost + 0.4
end
end
class Sugar
include Decorator
def cost
@component.cost + 0.2
end
end
coffee = Coffee.new
Sugar.new(Milk.new(coffee)).cost # 2.6
Sugar.new(Sugar.new(coffee)).cost # 2.4
Sugar.new(Milk.new(coffee)).origin # Colombia
Sugar.new(Milk.new(coffee)).class # Sugar