假设我有这个课:
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
答案 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
。这有两个原因:
这只是each
的合同。 Ruby中实现each
的每个其他对象都返回self
。如果是Java,那么它将是Iterable
接口的一部分。
我实际上会无意间泄漏我努力保护的内部状态!正如我刚写的:each
的每个实现都返回self
,那么@numbers.each
返回什么?它返回@numbers
,这意味着我整个Example#each
方法都返回@numbers
,这正是我要隐藏的东西!
<<
I 可以控制内部状态,而不是分发内部状态并添加 caller 。我实现了<<
的 own 版本,可以在其中检查所需内容,并确保不违反对象的不变性。
请注意,我返回了self
。这有两个原因:
这只是<<
的合同。 Ruby中实现<<
的每个其他对象都返回self
。如果是Java,那么它将是Appendable
接口的一部分。
我实际上会无意间泄漏我努力保护的内部状态!正如我刚写的:<<
的每个实现都返回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