This is an old pet peeve of mine: again and again, I see people failing to grasp what a decorator is. I don’t have any data to back me up here, but I would say that it is the most misunderstood design pattern of all. I’ve seen it being abused by several communities, including my beloved Ruby, in ways that not only change its specification, but defeat completely its purpose. Here’s how the usual “decorator” goes:
class UserDecorator
def initialize(user)
@user = user
end
def email
@user.email
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
def full_address
"#{@user.address.number} #{@user.address.street}, #{@user.address.city}, #{@user.address.state}"
end
end
User = Struct.new(:first_name, :last_name, :email, :address)
Address = Struct.new(:number, :street, :city, :state)
user_decorator = UserDecorator.new(
User.new(
"Oddly",
"Functional",
"hi@oddlyfunctional.com",
Address.new("123", "St. Nowhere", "New York", "NY")
)
)
user_decorator.email
user_decorator.full_name
user_decorator.full_address
As it is commonly known, a decorator is a presentational component that wraps a model instance and exposes proper methods for presentational purposes (for example, formatting the full address or the full name, while delegating the methods that are not going to be changed to the wrapped instance, as is the case for the email). I say, with a dash of irritation, that this is not a decorator. You could call it a presenter or something else, but its goal and usage are completely distinct from a decorator’s. This misconception is reinforced by the community as gems (yeah, I’m looking at you, Draper 👀), and results in fewer and fewer developers knowing what a decorator really is.
But what is it after all?
For a formal definition, you can check the original Gang of Four’s Design Patterns book, but put simply, a decorator is a class that wraps an instance and implements a well defined interface common to that instance, in order to dynamically and transparently add behaviour to the wrapped instance in a composable manner. “LOL, you look like my teacher, easier said than done” you must be thinking. It’s actually really simple and practical. Buckle up, I’m going to show you some code!
require 'forwardable'
class UserContactEmailDecorator
extend Forwardable
def_delegators :@user, :first_name, :last_name
def initialize(user)
@user = user
end
def email
"#{full_name} <#{@user.email}>"
end
private
def full_name
"#{@user.first_name} #{@user.last_name}"
end
end
class UserUppercaseNamesDecorator
extend Forwardable
def_delegators :@user, :email
def initialize(user)
@user = user
end
def first_name
@user.first_name.upcase
end
def last_name
@user.last_name.upcase
end
end
User = Struct.new(:first_name, :last_name, :email)
user = User.new("Oddly", "Functional", "hi@oddlyfunctional.com")
decorated_user = UserContactEmailDecorator.new(UserUppercaseNamesDecorator.new(user))
decorated_user.email
decorated_user.first_name
decorated_user.last_name
decorated_user = UserUppercaseNamesDecorator.new(UserContactEmailDecorator.new(user))
decorated_user.email
decorated_user.first_name
decorated_user.last_name
decorated_user = UserContactEmailDecorator.new(user)
decorated_user.email
decorated_user.first_name
decorated_user.last_name
decorated_user = UserUppercaseNamesDecorator.new(user)
decorated_user.first_name
decorated_user.last_name
decorated_user.email
Differently from the previous, erroneously called decorator, actual decorators allow the programmer to compose arbitrary behaviours at runtime, benefiting from the indirection of not knowing which class is being received, and having the confidence that any instance of any decorator and of the original class will implement to the same common interface. It allows indefinitely nesting, which is kind of awesome (rack, anyone?). That’s impossible to achieve when changing the interface by adding or removing methods, since the client class or the caller wouldn’t be able to treat any potentially decorated instance as a member of the defined common interface.
While these examples still implement different ways to present the model, there’s nothing in the decorator pattern that makes any reference to how the class is going to be used. To prove that, here follows a use case that doesn’t involve a presentational context:
class Operator
def run
end
end
class OperationLoggerDecorator
def initialize(operator, logger)
@operator = operator
@logger = logger
end
def run
@logger.info "Initiating operation..."
result = @operator.run
@logger.info "Finished with result: #{result}"
result
end
end
class OperationNotifierDecorator
def initialize(operator)
@operator = operator
end
def run
result = @operator.run
Notification.create("Operation finished with result: #{result}")
result
end
end
operator = Operator.new
operator.run
OperationLoggerDecorator.new(operator).run
OperationNotifierDecorator.new(operator).run
OperationLoggerDecorator.new(OperationNotifierDecorator.new(operator)).run
OperationNotifierDecorator.new(OperationLoggerDecorator.new(operator)).run
Settings = Struct.new(:log?, :notify?)
settings = Settings.new(true, true)
if settings.log?
operator = OperationLoggerDecorator.new(operator)
end
if settings.notify?
operator = OperationNotifierDecorator.new(operator)
end
operator.run
Phew, this is a weight off my shoulders! I’ve been annoyed by this common misconception for so long, but never took the time to write about it. It feels so fine it’s almost therapeutic!
I hope you can now appreciate decorators for what they really are. You could argue that they lead to too much indirection or that they are overkill solutions for simple cases (and you probably would be right). You have the right not to like it and decide not to use it. But please, please, don’t call a presenter a decorator.
I must add an addendum and say that I don’t hate presenters. They are a great way to manage certain complexities and avoid bloating your views, but names and definitions are important.
Did you like my article? Follow me on Twitter at @oddlyfunctional