最近,我发现了一些使用Rails的delegate 方法的旧代码。看看下面这个例子,我把几个方法委托给一个实例变量,但想把它们变成私有的。
class UserDecorator
def initialize(user)
@user = user
end
def full_name
"#{first_name} #{last_name}"
end
private
delegate :first_name, :last_name, to: :@user
end
我的意图是把first_name 和last_name 方法变成私有的。只有full_name 应该是公开的。 让我们看看这是否按预期的那样工作。
user = User.new(first_name: 'John', last_name: 'Doe')
decorated_user = UserDecorator.new(user)
decorated_user.full_name #=> John Doe
decorated_user.first_name #=> John
decorated_user.last_name #=> Doe
decorated_user.methods - Object.instance_methods
#=> [:full_name, :last_name, :first_name]
正如你所看到的,我们可以调用这两个委托方法。 它们应该是私有的!这里发生了什么?
问题是把delegate 放在private 作用域下没有任何效果。Ruby 中的方法在从类中调用时没有办法知道private 的可见性作用域被设置了。delegate 使用module_eval 来定义这些名字的新方法,但它不知道它必须把这些方法变成私有的。
那么,如果我们希望委托一个方法而不使其成为类的公共接口的一部分,我们该怎么做呢? 让我们深入了解一下delegate 。在这样的情况下,我喜欢使用pry控制台来交互地检查类的行为。
pry> cd UserDecorator
# This pry command puts us within the scope of the UserDecorator class.
pry> delegate :first_name, :last_name, to: :@user
#=> [:first_name, :last_name]
pry> instance_methods
#=> [:full_name, :last_name, :first_name]
pry> private :first_name, :last_name
pry> instance_methods
#=> [:full_name]
delegate 返回委托方法名称的列表。 我们可以用这些方法名称调用 ,使其成为私有的。这意味着我们可以像这样明确地使这些方法成为私有。private
class UserDecorator
# def initialize...
delegate :first_name, :last_name, to: :@user
private :first_name, :last_name
end
现在,我们怎样才能避免方法名列表的重复呢? 由于delegate 返回方法名列表,我们可以使用splat操作符将列表传递给private。
class UserDecorator
# def initialize...
private *delegate(:first_name, :last_name, to: :@user)
end
虽然这可以避免重复,但我觉得这种语法不是很直观。 幸运的是,Rails 6正在引入一个新的private: true 关键字参数,给我们提供一个更简洁的语法。
# Rails 6+
delegate :first_name, :last_name, to: :@user, private: true
然而,在我写这篇文章的时候,Rails 6还没有出来。 如果你使用的是Rails 5.2或以下版本,你现在必须使用private *delegate(...) 语法。