【问题标题】:Should callers check validity of arguments before calling constructor?调用者应该在调用构造函数之前检查参数的有效性吗?
【发布时间】:2013-03-11 11:31:53
【问题描述】:

我最近阅读了很多关于 TDD 和干净代码的内容,因此我开始着手一个简单的项目,将它们投入使用,但我遇到了一些我真的不确定最好的方法是什么。

我有一个将Java File 对象作为参数的类,期望这个File 对象必须是一个目录并且必须以某个前缀开头。我的第一次通过涉及在调用构造函数之前对 File 对象进行检查,即检查它是否是一个目录并检查名称是否有效。但我不喜欢调用者指定什么使它有效,特别是有效前缀是什么,我认为这个逻辑应该放在类本身中。

我可以在构造函数中执行此检查并在它无效时抛出异常,但鉴于问题的性质,如果我正在迭代 Files 的列表,那么完全可以预期其中一些不会是“有效的”(即它们将是文件而不是目录)所以真的有必要抛出 Exception 吗?

public MyObject(File directory) {
    if (!directory.isDirectory()) {
        throw new IllegalArgumentException("Must be a directory");
    }
    if (!directory.getName().startsWith("Prefix")) {
        throw new IllegalArgumentException("Must start with Prefix");
    }
    ....
}

我考虑过添加一个工厂方法来创建对象并在File 无效时返回 null。

public static MyObject createMyObject(File directory) {
    if (!directory.isDirectory() || !directory.getName().startsWith("Prefix")) {
        return null;
    }
    return new MyObject(directory);
}

另外,我考虑在类中添加一个静态方法,在调用构造函数之前为调用者验证文件。

public static boolean isValid(File directory) {
    return directory.isDirectory() && directory.getName().startsWith("Prefix");
}

if (MyObject.isValid(directory)) {
    MyObject object = new MyObject(directory);
}

那么就干净的代码和所有 OOP 原则(例如单一职责、耦合等)而言,哪种方式是首选方式?

更新:

阅读了一些已经发布的答案后,我开始考虑另一种可能性,这种可能性仅适用于我目前的情况,而不是我的问题所涉及的一般情况。

作为我的调用代码的一部分,我有一个来自文件系统的路径,我列出了该目录中的所有文件,然后我将每个文件传递给 MyObject 构造函数,无论它是否有效。我可以将FileFilter 传递给listFiles 方法,以确保listFiles 只返回有效目录。 FileFilter 可以在 MyObject 中声明:

public static FileFilter getFilter() {
    return new FileFilter() {
        public boolean accept(File path) {
            return path.isDirectory() && path.getName().startsWith("Prefix");
        }
    };
}

如果我的构造函数抛出异常,那么这确实是一种异常情况,因为期望它只被传递有效目录。这样做意味着我可以消除对构造函数/工厂检查异常的需要,因为任何异常都表明某处存在错误,而不是预期的行为。但它仍然留下了是否将其放入构造函数或工厂方法的问题。

【问题讨论】:

  • 对我来说,第三个会更好。更短,您可以将它与许多文件一起使用。此外,您不需要从 isValid 方法返回新对象,并且仅当 isValid 从代码的另一部分返回 true 时才创建对象。这很好,因为它分离了功能。
  • @AliAlamiri 就干净的代码而言,这是我的偏好,因为阅读它是最有意义的,即读者很清楚正在发生什么。我只是不能决定我是否喜欢调用者必须知道他应该先调用 isValid 的事实。
  • 为什么不将文件传递给构造函数并在那里检查文件是否有效。当您构造对象并让构造函数检查正在传递的文件时,这会隐藏调用者的检查?
  • @AliAlamiri 是的,这是其中一种选择,但我只是想知道在构造函数中抛出异常的最佳实践。

标签: java oop coding-style


【解决方案1】:
public static MyObject createMyObject(File directory) throws IllegalArgumentException{
    if (!directory.isDirectory() || !directory.getName().startsWith("Prefix")) {
        return throw new IllegalArgumentException("invalid parameters")";
    }
    return new MyObject(directory);
}

这是您建议的两个选项的混合选项之一。使用工厂方法并验证工厂方法擅长的前提条件。所以 IMO 这可能是一个不错的选择。

为什么不选择选项 2: 因为当您可以抛出异常以警告用户某些先决条件未满足时,返回 nulls 是一个不好的选择。

更新: 这种策略保证只会创建一个处于有效状态的对象。 此外,如果你想模拟出MyObject 的实例,你可以让它实现一些使用运行时多态性的接口并传递模拟对象。希望这是有道理的。

【讨论】:

【解决方案2】:

我更喜欢提供构造函数的组合,该构造函数在给定的参数未满足其契约时验证并引发异常,并由 static boolean isValid() 验证方法补充。

在循环中使用验证方法提供了一种很好的、​​可读的方式来构造有效对象,而构造函数确保通过在需要时抛出异常来处理未验证的使用。

【讨论】:

    【解决方案3】:

    我认为验证代码的归属取决于“MyObject”类所代表的确切含义。

    如果 MyObject 执行一些操作如果它有一个文件而不是一个目录就会失败,那么我会说它应该在其构造函数中包含验证代码 - 这使得类自包含,并允许可能的重用稍后上课。

    如果 MyObject 只是文件/目录的容器,并且其中没有特定于目录的代码,则将验证代码放在确实需要目录而不是文件的类中.

    【讨论】:

    • 不鼓励将验证代码保留在构造函数中,因为您在测试时会遇到这种行为,并且您只想传递一个模拟,这在此处是不可能的。当您尝试使用一些模拟值创建模拟时,这些模拟值必须有效才能使实例化成功。
    • 我认为在构造函数中检查参数是相当标准的做法,Joshua Bloch 在 Effective Java 中当然强烈鼓励它:“构造函数代表了一个特殊情况,即您应该检查要执行的参数的有效性。存储起来以备后用。检查构造函数参数的有效性至关重要,以防止构造违反其类不变量的对象。"
    • 构造函数应该只包含赋值,前置条件验证应该通过工厂方法或对象的工厂来完成。 misko.hevery.com/code-reviewers-guide/… 这是推广这种方法的好资源。
    • 我可以从可测试性的角度看到好处,但我们真的是说 'new Integer("not a number")' 应该在没有任何错误的情况下运行吗?
    • 没有。在这种情况下,获取整数实例的唯一方法是使用工厂方法或工厂类。
    【解决方案4】:

    这取决于...

    从编码的角度来看,最简单和最干净的方法是爆炸式(未开发的)构造函数。如果有合理的期望调用者会并且应该只传递目录,那么就这样做吧。

    如果有合理的期望调用者可以传入非目录,那么您有两种选择:

    1. 如果调用者可以处理这种情况,让构造函数抛出一个checked异常
    2. 如果调用者无法处理异常,如果调用者调用方法,您可以让您的类“什么也不做”——如果传递给构造函数的文件不是目录,则基本上忽略所有“做”某事的请求。

    【讨论】:

    • 所以想法是给定文件系统中的路径,代码将遍历其内容并创建具有给定前缀的所有目录的列表。因此,期望某些内容无效是完全合理的,因此我不希望将这些文件/目录添加到列表中。我只是不确定是否要向构造函数添加异常,或者使用工厂是否更好。
    • 工厂与否,你需要决定谁来处理这个问题。一旦你弄清楚了,方法应该是显而易见的。请注意,在这种情况下,工厂无济于事——您仍然有相同的选择,只是转移到了不同​​的方法。工厂仅在您需要在构造对象后对其进行某些操作时才有用(以避免在将this 从构造函数中传递给方法时出现“让this 逃逸”问题)。
    【解决方案5】:

    这称为Design-by-Contract,并且有一些库可以帮助您检查传入的参数。

    【讨论】:

      猜你喜欢
      • 2017-06-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-17
      • 2012-09-28
      • 1970-01-01
      • 2014-06-11
      相关资源
      最近更新 更多