从访问者那里抛出异常的糟糕设计决定?

时间:2011-09-07 13:05:28

标签: java design-decisions

我已经阅读了一些关于在访问者中抛出异常的专家和骗子的答案,但我想我会用一个例子来讨论我的具体问题:

public class App {
  static class Test {       
    private List<String> strings;

    public Test() {
    }

    public List<String> getStrings() throws Exception {
        if (this.strings == null)
            throw new Exception();

        return strings;
    }

    public void setStrings(List<String> strings) {
        this.strings = strings;
    }
}

public static void main(String[] args) {
    Test t = new Test();

    List<String> s = null;
    try {
        s = t.getStrings();
    } catch (Exception e) {
        // TODO: do something more specific
    }
  }
}

getStrings()Exception未设置时抛出strings。这种情况会通过方法更好地处理吗?

8 个答案:

答案 0 :(得分:6)

是的,那很糟糕。我建议在声明中初始化strings变量,如:

private List<String> strings = new ArrayList<String>();

你可以用

之类的东西替换setter
public void addToList(String s) {
     strings.add(s);
}

因此不可能有NPE。

(或者不要在声明中初始化strings变量,将初始化添加到addToList方法,并使用它来检查getStrings()以查看它是否返回null。)

至于为什么它很糟糕,用这样一个人为的例子很难说,但似乎有太多的状态改变,你通过让用户处理异常来惩罚这个类的用户。还有一个问题是,一旦程序中的方法开始抛出异常,那么它往往会转移到整个代码库中。

检查此实例成员是否已填充需要在某个地方完成,但我认为应该在某个地方完成,这个知识比正在这个类中有更多的知识。

答案 1 :(得分:6)

这实际上取决于您获得的列表正在做什么。我认为更优雅的解决方案是返回Colletions.EMPTY_LIST,或者@Nathan建议,返回空ArrayList。然后,使用该列表的代码可以检查它是否为空,如果它想要,或者只是像使用任何其他列表一样使用它。

区别在于没有字符串之间没有字符串列表。第一个应该由空列表表示,第二个应该返回null或抛出异常。

答案 2 :(得分:5)

正确处理此问题的方法,假设要求在调用getter之前将其设置在其他位置,如下所示:

public List<String> getStrings() {
    if (strings == null)
        throw new IllegalStateException("strings has not been initialized");

    return strings;
}

作为未经检查的异常,不需要在方法上声明它,因此您不会使用大量的try / catch噪声污染客户端代码。此外,因为这个条件是可以避免的(即客户端代码可以确保列表已经初始化),所以它是编程错误,因此未经检查的异常是可接受的,也是可取的。

答案 3 :(得分:1)

一般来说,这是一种设计气味。

如果字符串未初始化确实是一个错误,那么要么删除setter,要么在构造函数中强制执行约束,要么像Bohemian所说的那样做,并抛出带有解释性消息的IllegalArgumentException。如果你做前者,你会得到快速失败的行为。

如果不是错误,则字符串为空,则考虑将列表初始化为空列表,并在setter中将其强制为非null,与上述类似。这样做的好处是您不需要在任何客户端代码中检查null。

for (String s: t.getStrings()) {
  // no need to check for null
}

这可以大大简化客户端代码。

编辑:好的,我刚看到你正在从JSON序列化,所以你无法删除构造函数。所以你需要:

  1. 从getter中抛出IllegalArgumentException
  2. 如果strings为null,则返回Collections.EMPTY_LIST。
  3. 或更好:在反序列化之后添加一个验证检查,在那里你专门检查字符串的值,并抛出一个合理的异常。

答案 4 :(得分:1)

我认为你已经揭露了原始问题背后的一个更重要的问题:你应该努力防止吸气剂和制定者的特殊情况吗?

答案是肯定的,你应该努力避免琐碎的例外。

你在这里有效的lazy instantiation在这个例子中根本没有动机:

  

在计算机编程中,延迟初始化是延迟的策略   创建对象,计算值或其他值   昂贵的过程,直到第一次需要它。

此示例中有两个问题:

  1. 您的示例不是线程安全的。检查null可以同时在两个线程上成功(即,发现对象为空)。然后,您的实例化会创建两个不同的字符串列表。随后将出现非确定性行为。

  2. 此示例中没有充分理由推迟实例化。这不是一项昂贵或复杂的操作。这就是我所说的“努力避免琐碎的异常”:值得投资周期来创建一个有用的(但是空的)列表,以确保你不会抛出延迟爆炸空指针异常。

  3. 请记住,当您在外部代码上造成异常时,您基本上希望开发人员知道如何使用它做一些合理的事情。您还希望希望在等式中没有第三个开发人员将其包装在异常食物中,只是为了捕获并忽略像您这样的异常:

    try {
        // I don't understand why this throws an exception.  Ignore.
        t.getStrings();
    } catch (Exception e) {
        // Ignore and hope things are fine.
    }
    

    在下面的示例中,我使用Null Object pattern向未来的代码表明您的示例尚未设置。但是,Null对象作为非特殊代码运行,因此不会对未来开发人员的工作流程产生开销和影响。

    public class App {   
        static class Test {            
            // Empty list
            public static final List<String> NULL_OBJECT = new ArrayList<String>();
    
            private List<String> strings = NULL_OBJECT;
    
            public synchronized List<String> getStrings() {
                return strings;
            }      
    
            public synchronized void setStrings(List<String> strings) {         
                this.strings = strings;     
            } 
        }  
    
        public static void main(String[] args) {     
            Test t = new Test();      
    
            List<String> s = t.getStrings();
    
            if (s == Test.NULL_OBJECT) {
                // Do whatever is appropriate without worrying about exception 
                // handling.
    
                // A possible example below:
                s = new ArrayList<String>();
                t.setStrings(s);
            }
    
            // At this point, s is a useful reference and 
            // t is in a consistent state.
        }
    }
    

答案 5 :(得分:0)

我会就此咨询Java Beans规范。可能最好不要抛出java.lang.Exception,而是使用java.beans.PropertyVetoException并让你的bean注册为java.beans.VetoableChangeListener并触发相应的事件。这有点过分,但会正确识别你想要做的事情。

答案 6 :(得分:0)

这实际上取决于你想要做什么。如果您试图强制执行在调用getter之前调用该设置的条件,则可能会出现抛出IllegalStateException的情况。

答案 7 :(得分:-1)

我建议从setter方法中抛出异常。有什么特别的理由为什么不这样做会更好?

public List<String> getStrings() throws Exception {
    return strings;
}

public void setStrings(List<String> strings) {
    if (strings == null)
        throw new Exception();

    this.strings = strings;
}

此外,根据特定的上下文,是不是可以直接通过构造函数传递List<String> strings?这样你甚至可能不需要setter方法。