可怜的、被误解的装饰器
一种最容易被误解的设计模式,通过实际示例进行解释。
这一直是我感到恼火的一点:我一次又一次地看到人们无法理解什么是装饰器。我没有数据来支持这一点,但我认为它是所有设计模式中最容易被误解的。我看到许多社区,包括我热爱的 Ruby,都在滥用它,不仅改变了它的规范,而且完全违背了它的目的。通常的“装饰器”是这样的
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
# => "hi@oddlyfunctional.com"
user_decorator.full_name
# => "Oddly Functional"
user_decorator.full_address
# => "123 St. Nowhere, New York, NY"
如大家所知,装饰器是一个表现层组件,它包装一个模型实例,并暴露适当的方法用于表现目的(例如,格式化完整的地址或完整的姓名,同时将不会被更改的方法委托给包装的实例,就像电子邮件的情况一样)。我说,带着一丝恼怒,这不是一个装饰器。你可以称它为 presenter(表现器)或其他什么,但它的目标和用法与装饰器完全不同。这种误解被社区强化,例如 gems(是的,我看着你,Draper 👀),导致越来越少的开发者知道装饰器到底是什么。
但到底是什么呢?
对于正式的定义,你可以查阅最初的 Gang of Four 的 设计模式一书,但简单来说,装饰器是一个包装实例的类,它实现了一个与该实例通用的明确接口,以便以可组合的方式动态且透明地向包装的实例添加行为。“哈哈,你听起来像我的老师,说起来容易做起来难”你可能在想。实际上这非常简单且实用。系好安全带,我将向你展示一些代码!
# I'm using Forwardable, a standard lib module, to make it easier to delegate
# methods that don't change to their original implementation.
# Check its documentation at:
# https://doc.ruby-lang.org.cn/stdlib-2.3.1/libdoc/forwardable/rdoc/Forwardable.html
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
# I'm leaving the address out since I'm not gonna use it in this example
User = Struct.new(:first_name, :last_name, :email)
user = User.new("Oddly", "Functional", "hi@oddlyfunctional.com")
# We can compose the decorators as we want
decorated_user = UserContactEmailDecorator.new(UserUppercaseNamesDecorator.new(user))
decorated_user.email
# => "ODDLY FUNCTIONAL <hi@oddlyfunctional.com>"
decorated_user.first_name
# => "ODDLY"
decorated_user.last_name
# => "FUNCTIONAL"
# You probably guessed that the order matters
decorated_user = UserUppercaseNamesDecorator.new(UserContactEmailDecorator.new(user))
decorated_user.email
# => "Oddly Functional <hi@oddlyfunctional.com>" # Different!
decorated_user.first_name
# => "ODDLY"
decorated_user.last_name
# => "FUNCTIONAL"
# We can also use them separately
decorated_user = UserContactEmailDecorator.new(user)
decorated_user.email
# => "Oddly Functional <hi@oddlyfunctional.com>"
decorated_user.first_name
# => "Oddly"
decorated_user.last_name
# => "Functional"
decorated_user = UserUppercaseNamesDecorator.new(user)
decorated_user.first_name
# => "ODDLY"
decorated_user.last_name
# => "FUNCTIONAL"
decorated_user.email
# => "hi@oddlyfunctional.com"
与之前错误地称为装饰器的不同,真正的装饰器允许程序员在运行时组合任意行为,受益于不知道接收的是哪个类的间接性,并确信任何装饰器和原始类的实例都将实现相同的通用接口。它允许无限嵌套,这很棒(rack,有人吗?)。当通过添加或删除方法来更改接口时,这是无法实现的,因为客户端类或调用者将无法将任何潜在的装饰实例视为定义的通用接口的成员。
虽然这些示例仍然以不同的方式呈现模型,但装饰器模式中没有任何内容引用类将如何使用。为了证明这一点,以下是一个不涉及表现层上下文的用例
class Operator
def run
# Do something
end
end
class OperationLoggerDecorator
def initialize(operator, logger)
@operator = operator
# An important point to note is that having the same interface
# does not mean having the same constructor. Whichever client code
# that is instantiating the decorator *knows* what it is doing.
@logger = logger
end
def run
@logger.info "Initiating operation..."
result = @operator.run
@logger.info "Finished with result: #{result}"
result # Returning the result to be used by the client
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
# I can freely compose the decorators!
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
# Or, in a more realistic manner:
Settings = Struct.new(:log?, :notify?)
# In a real application, the settings would be
# stored somewhere, probably in the database.
settings = Settings.new(true, true)
if settings.log?
operator = OperationLoggerDecorator.new(operator)
end
if settings.notify?
operator = OperationNotifierDecorator.new(operator)
end
operator.run
呼,这让我松了一口气!我对此常见的误解感到恼火已久,但从未花时间写下来。感觉太好了,几乎是治疗!
我希望你现在能够欣赏装饰器真正的价值。你可能会争辩说它们会导致过多的间接性,或者对于简单的案例来说它们是过度设计的解决方案(你可能是对的)。你有权不喜欢它并决定不使用它。但是,请不要把 presenter 称为装饰器。
我必须补充一点,我并不讨厌 presenter。它们是管理某些复杂性并避免使你的视图膨胀的好方法,但名称和定义很重要。
你喜欢我的文章吗?在 Twitter 上关注我:@oddlyfunctional