覆盖实例变量的<<方法

时间:2020-10-15 21:01:21

标签: ruby

假设我有这个课:

class Example
  attr_accessor :numbers

  def initialize(numbers = [])
    @numbers = numbers
  end

  private

  def validate!(number)
    number >= 0 || raise(ArgumentError)
  end
end

我想先将#validate!用于任何新数字,然后再将其推入numbers

example = Example.new([1, 2, 3])
example.numbers # [1, 2, 3]
example.numbers << 4
example.numbers # [1, 2, 3, 4]
example.numbers << -1 # raise ArgumentError

下面是我能做的最好的事情,但是我真的不确定。

此外,它仅适用于<<,不适用于push。我可以添加它,但是有无限循环的危险...)。

是否有更“常规”的方式来做到这一点?我找不到任何正式程序。

class Example
  attr_accessor :numbers

  def initialize(numbers = [])
    @numbers = numbers
    bind = self # so the instance is usable inside the singleton block
    @numbers.singleton_class.send(:define_method, :<<) do |value|
      # here, self refers to the @numbers array, so use bind to refer to the instance
      bind.send(:validate!, value)
      push(value)
    end
  end

  private

  def validate!(number)
    number >= 0 || raise(ArgumentError)
  end
end

1 个答案:

答案 0 :(得分:0)

编程与现实生活非常相似:只是四处奔走,让陌生人触摸您的私密部分不是一个好主意。

您正在解决错误的问题。您正在尝试规范陌生人在玩弄您的私人物品时可以做什么,但是相反,您根本不应该让他们首先触摸您的私人物品。

class Example
  def initialize(numbers = [])
    @numbers = numbers.clone
  end

  def numbers
    @numbers.clone.freeze
  end

  def <<(number)
    validate(number)
    @numbers << number
    self
  end

  private

  def validate(number)
    raise ArgumentError, "number must be non-negative, but is #{number}" unless number >= 0
  end
end

example = Example.new([1, 2, 3])
example.numbers # [1, 2, 3]
example << 4
example.numbers # [1, 2, 3, 4]
example << -1 # raise ArgumentError

让我们逐个查看一下我所做的所有更改。

clone设置初始化参数

您正在从不受信任的源(调用方)获取可变对象(数组)。您应该确保呼叫者不能做任何“鬼nea”的事情。在您的第一个代码中,我可以这样做:

ary = [1, 2, 3]
example = Example.new(ary)

ary << -1

由于您只是拿了我交给您的 my 阵列,所以我仍然可以对阵列进行任何所需的操作!

即使在强化版本中,我也可以这样做:

ary = [1, 2, 3]
example = Example.new(ary)

class << ary
  remove_method :<<
end

ary << -1

或者,在将数组交给您之前,我可以freeze进行数组操作,这使得无法向其添加单例方法。

即使没有安全方面,您也应该 这样做,因为您违反了另一个现实生活规则:不要玩别人的玩具!我递给您 my 数组,然后将其变异。在现实世界中,这将被视为不礼貌。在编程中,这是令人惊讶的,并且会令人惊奇。

clone进入吸气剂

这很重要:@numbers数组是我私有的内部状态。我应该永远不要把它交给陌生人。如果您不分发@numbers数组,那么您所防范的所有问题都不会发生。

您正试图防止陌生人改变您的内部状态,而解决方案很简单:不要给陌生人您的内部状态!

freeze在技术上不是必需的,但我希望它向调用者表明这只是对example对象状态的查看,并且只允许他们查看什么我希望他们这么做。

再说一次,即使没有安全方面,这仍然是一个坏主意:通过将内部实现公开给客户,您将无法在不破坏客户的情况下更改内部实现。如果将数组更改为链接列表,则客户将无法使用,因为它们习惯于获取可以随机索引的数组,但不能随机索引链接列表,因此始终必须从列表中遍历它。

不幸的是,该示例太小且太简单,无法判断,但是我什至会质疑为什么首先要分发数组。客户想用这些数字做什么?也许对它们进行迭代就足够了,在这种情况下,您不需要给它们一个完整的数组,只需一个迭代器即可:

class Example
  def each(...)
    return enum_for(__callee__) unless block_given?
    @numbers.each(...)
    self
  end
end

如果调用方想要一个数组,他们仍然可以通过在to_a上调用Enumerator来轻松获得一个数组。

请注意,我返回了self。这有两个原因:

  1. 这只是each的合同。 Ruby中实现each的每个其他对象都返回self。如果是Java,那么它将是Iterable接口的一部分。

  2. 我实际上会无意间泄漏我努力保护的内部状态!正如我刚写的:each的每个实现都返回self,那么@numbers.each返回什么?它返回@numbers,这意味着我整个Example#each方法都返回@numbers,这正是我要隐藏的东西!

亲自<<

I 可以控制内部状态,而不是分发内部状态并添加 caller 。我实现了<< own 版本,可以在其中检查所需内容,并确保不违反对象的不变性。

请注意,我返回了self。这有两个原因:

  1. 这只是<<的合同。 Ruby中实现<<的每个其他对象都返回self。如果是Java,那么它将是Appendable接口的一部分。

  2. 我实际上会无意间泄漏我努力保护的内部状态!正如我刚写的:<<的每个实现都返回self,那么@numbers << number返回什么?它返回@numbers,这意味着我整个Example#<<方法都返回@numbers,这正是我要隐藏的东西!

丢下爆炸

在Ruby中,以bang结尾的方法名称表示“此方法比非bang方法更令人惊讶”。在您的情况下,没有 非爆炸对象,因此该方法不应爆炸。

不要滥用布尔运算符来控制流

…或者至少(如果这样做的话)使用关键字版本(and / or)而不是符号版本(&& / ||)。

但实际上,您应该完全将其作废。 do or die在Perl中是惯用的,但在Ruby中却不是。

从技术上讲,我已更改了您方法的返回值:它曾经返回true为有效值,现在它返回了nil。但是无论如何,您都忽略它的返回值,所以没关系。

但是,

validate可能不是该方法的好名字。我希望一个名为validate的方法返回一个布尔结果,而不引发异常。

特殊信息

您应该在异常中添加消息,以告诉程序员出了什么问题。另一种可能性是创建更具体的例外,例如

class NegativeNumberError < ArgumentError; end

但是在这种情况下,这将是过大的。通常,如果您希望 code 可以“读取”您的异常,请创建一个新类,如果您希望 humans 读取您的异常,那么一条消息就足够了。

封装,数据抽象,信息隐藏

这是三个微妙但相关的概念,它们是编程中最重要的概念之一。我们总是 希望隐藏我们的内部状态,并封装将其隐藏在我们控制的方法之后。

最大封装

有些人(包括我自己)甚至都不喜欢物体本身以其内部状态运行。就个人而言,我什至封装了私有实例变量,这些私有实例变量永远不会暴露在getter和setter之后。原因是这使类更易于子类化:您可以覆盖和专门化方法,而不能覆盖实例变量。因此,如果我直接使用实例变量,则子类无法“钩住”这些访问。

如果我使用getter和setter方法,则子类可以覆盖它们(或仅覆盖其中之一)。

注意:该示例太小和太简单,因此我想出一个好名字确实遇到了麻烦(示例中没有足够的知识来理解变量的用法和含义),所以最终,我刚刚放弃了,但是您会明白我使用getter和setter的意思:

class Example
  class NegativeNumberError < ArgumentError; end

  def initialize(numbers = [])
    self.numbers_backing = numbers.clone
  end

  def each(...)
    return enum_for(__callee__) unless block_given?
    numbers_backing.each(...)
    self
  end

  def <<(number)
    validate(number)
    numbers_backing << number
    self
  end

  private

  attr_accessor :numbers_backing

  def validate(number)
    raise NegativeNumberError unless number >= 0
  end
end

example = Example.new([1, 2, 3])
example.each.to_a # [1, 2, 3]
example << 4
example.each.to_a # [1, 2, 3, 4]
example << -1 # raise NegativeNumberError