前面我说过 ,构造函数必须是无代码的,并且除了属性初始化外什么也不能做。 从那时起,最常被问到的问题是:参数验证如何? 如果它们“损坏”,那么在“无效”状态下创建对象的意义何在? 这样的对象稍后将在意外的时刻失败。 在实例化的瞬间抛出异常不是更好吗? 要快速失败,可以这么说吗? 这就是我的想法。
让我们从以下Ruby代码开始:
class Users {
def initialize(file)
@file = file
end
def names
File.readlines(@file).reject(&:empty?)
end
}
我们可以使用它从文件中读取用户列表:
Users.new('all-users.txt').names
有很多滥用此类的方法:
- 将
nil
而不是文件名传递给ctor; - 传递其他东西,不是
String
; - 传递一个不存在的文件;
- 传递目录而不是文件。
您看到我们可以犯的这四个错误之间的区别吗? 让我们来看看我们的班级如何保护自己免受他们的侵害:
class Users {
def initialize(file)
raise "File name can't be nil" if file.nil?
raise 'Name must be a String' unless file.is_a?(String)
@file = file
end
def names
raise "#{@file} is absent" unless File.exist?(@file)
raise "#{@file} is not a file" unless File.file?(@file)
File.readlines(@file).reject(&:empty?)
end
}
前两个潜在的错误在构造函数中被滤除,而后两个潜在的错误在方法中被滤除。 我为什么要这样做? 为什么不将它们全部放入构造函数中?
因为前两个危害对象状态 ,而其他两个危害对象的运行时行为。 您还记得一个对象是它封装的一组其他对象(称为属性)的代表。 类Users
的对象不能表示 nil
或数字。 它只能代表名称类型为String
的文件。 另一方面,该文件包含的内容以及它实际上是否为文件,不会使状态无效。 这只会给行为造成麻烦。
即使差异看起来很细微,也很明显。 与封装的对象有两个交互阶段: connecting和talking 。
首先,我们封装file
并希望确保它确实是文件。 我们尚未讨论它,我们还不希望它为我们工作,我们只是想确保它确实是我们可以在不久的将来与之对话的对象。 如果是nil
或float
,那么将来肯定会有问题。 这就是为什么我们从构造函数中引发异常。
然后是第二阶段,我们将控制权委派给对象并期望其行为正确。 在此阶段,我们可能还有其他验证程序,以确保我们的交互将顺利进行。 重要的是要提到这些验证是非常实际的情况。 我们可能会多次调用names()
,并且每次磁盘上的文件都有不同的情况。 首先,它可能不存在,但几秒钟后便可以使用并可以读取了。
理想情况下,编程语言应提供用于第一类验证的工具,例如使用严格类型的验证。 例如,在Java中,我们不需要检查file
的类型,编译器会更早地捕获该错误。 由于Kotlin具有Null安全功能,因此我们可以摆脱NULL检查。 Ruby不如那些语言强大,这就是为什么我们必须“手动”验证的原因。
因此,总而言之,在构造函数中进行验证不是一个坏主意,只要验证不涉及对象,而仅确认它们足够好以供以后使用。
翻译自: https://www.javacodegeeks.com/2018/05/object-validation-defer-not.html