Ruby的隐藏宝石,代表和可转发

在今天对Ruby标准库中隐藏宝石的探索中,我们将着眼于委托。

不幸的是,这个术语就像许多其他一样 – 多年来变得有些混乱,对不同的人来说意味着不同的东西。根据维基百科:

委托是指在另一个原始对象(发送者)的上下文中评估一个对象(接收者)的成员(属性或方法)。可以通过将发送对象传递给接收对象来明确地完成委托,这可以使用任何面向对象的语言来完成; 或隐式地,由语言的成员查找规则,这需要语言支持该功能。

然而,人们通常也使用该术语来描述一个对象,该对象调用另一个对象的相应方法而不将其自身作为参数传递,这可以更准确地称为“转发”。

有了这个,我们将使用“委托”来描述本文其余部分的这两种模式。

委托者

让我们通过查看标准库的Delegatorclass开始我们在Ruby中的委托探索,该委托人提供了几种委托模式。

SimpleDelegator

其中最简单的,也是我在野外遇到的最简单的是SimpleDelegator,它盘点了一个通过初始化程序提供的对象,然后将所有缺少的方法委托给它。让我们看看这个行动:

require 'delegate'  User = Struct.new(:first_name, :last_name)  class UserDecorator < SimpleDelegator   def full_name     "#{first_name} #{last_name}"   end end 

首先,我们需要’委托’来使SimpleDelegatoravailable到我们的代码。我们还使用Structto使用first_name和last_nameaccessors创建一个简单的Userclass。然后我们添加了UserDecorator,它定义了一个将各个名称部分组合成一个字符串的full_name方法。这就是SimpleDelegator发挥作用的地方:因为当前类都没有定义first_namenor last_name,所以它们将在盘点对象上被调用:

decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe" 

SimpleDelegatoralso允许我们用super覆盖委托方法,在盘点对象上调用相应的方法。我们可以在我们的示例中使用它来仅显示首字母而不是完整的名字:

class UserDecorator < SimpleDelegator   def first_name     "#{super(0)}."   end end 
decorated_user.first_name #=> "J." decorated_user.full_name #=> "J. Doe" 

委托者

在阅读上面的例子时,您是否想知道我们的UserDecoratorknew如何委托给哪个对象?答案就在于SimpleDelegator的父类 – Delegator。这是一个抽象基类,用于通过提供__getobj__和__setobj__的实现来分别定义委托目标来定义自定义委派方案。利用这些知识,我们可以轻松地构建我们自己的SimpleDelegator版本以用于演示目的:

class MyDelegator < Delegator   attr_accessor :wrapped   alias_method :__getobj__, :wrapped    def initialize(obj)     @wrapped = obj   end end  class UserDecorator < MyDelegator   def full_name     "#{first_name} #{last_name}"   end end 

这与SimpleDelegator的初始化方法中调用__setobj__的实际实现略有不同。由于我们的自定义委托者类不需要它,我们完全省略了该方法。

这应该像我们之前的例子一样工作; 确实如此:

UserDecorator.superclass #=> MyDelegator < Delegator decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe" 

DelegateMethod

委托给我们的最后一个委托模式是有点奇怪的名称Object.DelegateClass方法。这将生成并返回特定类的委托者类,然后我们可以继承该类:

  class MyClass < DelegateClass(ClassToDelegateTo)     def initialize       super(obj_of_ClassToDelegateTo)     end   end 

虽然这可能看起来很混乱 – 尤其是继承的右侧可以包含任意Ruby代码的事实 – 它实际上遵循我们之前探索过的模式,即它类似于继承SimpleDelegator。

Ruby的标准库使用此功能来定义其Tempfileclass,它将大部分工作委托给Fileclass,同时设置一些有关存储位置和文件删除的特殊规则。我们可以使用相同的机制来设置这样的自定义Logfileclass:

class Logfile < DelegateClass(File)   MODE = File::WRONLY|File::CREAT|File::APPEND    def initialize(basename, logdir = '/var/log')     # Create logfile in location specified by logdir     path = File.join(logdir, basename)     logfile = File.open(path, MODE, 0644)      # This will call Delegator's initialize method, so below this point     # we can call any method from File on our Logfile instances.     super(logfile)   end end 

转发

有趣的是,Ruby的标准库以Forwardable模块及其def_delegator和def_delegators方法的形式为我们提供了另一个委托库。

让我们用Forwardable重写我们原来的UserDecorator示例。

require 'forwardable'  User = Struct.new(:first_name, :last_name)  class UserDecorator   extend Forwardable   def_delegators :@user, :first_name, :last_name    def initialize(user)     @user = user   end    def full_name     "#{first_name} #{last_name}"   end end  decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe" 

最明显的区别是委托不是通过method_missing自动提供的,而是需要为我们要转发的每个方法显式声明。这允许我们“隐藏”我们不希望向客户公开的盘点对象的任何方法,这使我们能够更好地控制我们的公共接口,这是我通常更喜欢Forwardable而不是SimpleDelegator的主要原因。

Forwardable的另一个不错的功能是能够通过def_delegator重命名委托方法,def_delegator接受一个指定所需别名的可选第三个参数:

class UserDecorator   extend Forwardable   def_delegator :@user, :first_name, :personal_name   def_delegator :@user, :last_name, :family_name    def initialize(user)     @user = user   end    def full_name     "#{personal_name} #{family_name}"   end end 

上述UserDecorator仅公开别名的personal_name和family_name方法,同时仍转发到盘点的User对象的first_name和last_name:

decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.first_name #=> NoMethodError: undefined method `first_name' for # decorated_user.personal_name #=> "John" 

这个功能有时会非常方便。我过去成功地使用过它,比如在类似接口的库之间迁移代码,但对方法名称有不同的期望。

标准图书馆外

尽管标准库中存在授权解决方案,但Ruby社区多年来已经开发了几种替代方案,接下来我们将探索其中的两种。

代表

考虑到Rails的流行,它的委托方法可能是Ruby开发人员使用的最常用的委托形式。以下是我们如何使用它来重写我们可靠的旧UserDecorator:

# In a real Rails app this would most likely be a subclass of ApplicationRecord User = Struct.new(:first_name, :last_name)  class UserDecorator   attr_reader :user   delegate :first_name, :last_name, to: :user    def initialize(user)     @user = user   end    def full_name     "#{first_name} #{last_name}"   end end  decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe" 

这与Forwardable非常相似,但是我们不需要使用extend,因为委托直接在Module上定义,因此可以在每个类或模块体中使用(无论好坏,你决定)。然而,代表有一些巧妙的伎俩。首先,有:prefix选项,它将委托的方法名称作为我们委托给的对象的名称的前缀。所以,

delegate :first_name, :last_name, to: :user, prefix: true 

将生成user_first_name和user_last_name方法。或者,我们可以提供自定义前缀:

delegate :first_name, :last_name, to: :user, prefix: :account 

我们现在可以访问用户名称的不同部分,如account_first_name和account_last_name。

委托的另一个有趣选项是:allow_nil选项。如果我们委托的对象当前是nil-例如由于未设置的ActiveRecord关系 – 我们通常会得到一个NoMethodError:

decorated_user = UserDecorator.new(nil) decorated_user.first_name #=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nil 

但是,使用:allow_nil选项,此调用将成功并返回nil:

class UserDecorator   delegate :first_name, :last_name, to: :user, allow_nil: true    ... end  decorated_user = UserDecorator.new(nil) decorated_user.first_name #=> nil 

铸件

我们将要看的最后一个委托选项是Jim Gay的Casting gem,它允许开发人员“在Ruby中委托方法并保留自己”。这可能是最接近严格的委托定义,因为它使用Ruby的动态性质暂时重新绑定方法调用的接收者,类似于:

UserDecorator.instance_method(:full_name).bind(user).call #=> "John Doe" 

最有趣的方面是开发人员可以向对象添加行为,而无需更改其超类层次结构。

require 'casting'  User = Struct.new(:first_name, :last_name)  module UserDecorator   def full_name     "#{first_name} #{last_name}"   end end  user = User.new("John", "Doe") user.extend(Casting::Client) user.delegate(:full_name, UserDecorator) 

在这里,我们使用Casting :: Client扩展了user,这使我们可以访问delegate方法。或者,我们可以使用包含Casting :: Clientinside的Userclass来为所有实例提供此功能。

此外,Casting提供了用于临时添加块生命周期的行为或直到再次手动删除的选项。为此,我们首先需要启用缺少方法的委派:

user.delegate_missing_methods 

要在单个块的持续时间内添加行为,我们可以使用Casting的委托类方法:

Casting.delegating(user => UserDecorator) do   user.full_name #=> "John Doe" end  user.full_name #NoMethodError: undefined method `full_name' for # 

或者,我们可以添加行为,直到我们明确地调用uncastagain:

user.cast_as(UserDecorator) user.full_name #=> "John Doe" user.uncast NoMethodError: undefined method `full_name' for # 

虽然稍微比其他解决方案更复杂,但Casting提供了很多控制,Jim在他的Clean Ruby书中展示了它的各种用途和更多用途。

摘要

委派和方法转发是用于在相关对象之间划分职责的有用模式。在纯Ruby项目中,可以使用Delegatorand Forwardable,而Rails代码往往倾向于其委托方法。为了最大程度地控制委托,Castinggem是一个很好的选择,尽管它比其他解决方案稍微复杂一些。

客座作者Michael Kohl对Ruby的热爱始于2003年左右。他还喜欢撰写和演讲该语言,并共同组织Bangkok.rb和RubyConf Thailand。

 

资讯来源:由0x资讯编译自DEV,原文:https://dev.to/appsignal/ruby-s-hidden-gems-delegator-and-forwardable-3pkh ,版权归作者所有,未经许可,不得转载

你可能还喜欢