如果getter对象的状态无效,是否应该抛出异常?


55

我经常遇到这个问题,特别是在Java中,即使我认为这是一个普遍的OOP问题。即:引发异常会揭示设计问题。

假设我有一个包含一个String name字段和一个String surname字段的类。

然后,它使用这些字段来组成一个人的全名,以便将其显示在某种文件上,例如发票。

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

现在,我想通过displayCompleteNameOnInvoice在分配名称之前调用引发错误来增强类的行为。似乎是个好主意,不是吗?

我可以在getCompleteName方法中添加引发异常的代码。但是以这种方式,我违反了与类用户的“隐式”合同。通常,如果未设置获取方法的值,则不应将其抛出异常。好的,这不是标准的获取方法,因为它不会返回单个字段,但是从用户的角度来看,区分可能太微妙了,无法考虑它。

或者,我可以从中抛出异常displayCompleteNameOnInvoice。但是,这样做我应该直接测试namesurname字段,这样做会违反表示的抽象getCompleteName。检查并创建完整名称是此方法的责任。根据其他数据,它甚至可以决定在某些情况下足够了surname

因此,唯一的可能性似乎是将方法的语义更改getCompleteNamecomposeCompleteName,这表明行为更“活跃”,并且具有抛出异常的能力。

这是更好的设计解决方案吗?我一直在寻找简单性和正确性之间的最佳平衡。有针对此问题的设计参考吗?


8
“一般而言,如果未设置其值,则不应该将getter引发异常。” -那他们该怎么办?
Rotem

2
@scriptin然后遵循该逻辑,displayCompleteNameOnInvoice如果getCompleteName返回则可以抛出异常null,不是吗?
Rotem

2
@Rotem可以。问题是是否应该这样做。
scriptin 2014年

22
在这个主题上,虚假程序员相信名字是关于为什么“名字”和“姓氏”是非常狭窄的概念的很好的读物。
Lars Viklund

9
如果您的对象可以具有无效状态,则说明您已经搞砸了。
user253751 2014年

Answers:


151

不要在没有分配名称的情况下构造您的类。


9
这很有趣,但却是一个彻底的解决方案。很多时候,您的类必须更加流畅,只有在调用某些方法时才允许状态成为问题,否则最终将导致小类的泛滥,而这需要使用过多的解释。
AgostinoX 2014年

71
这不是评论。如果他在答案中应用建议,那么他将不再遇到上述问题。这是一个答案。
DeadMG

14
@AgostinoX:仅在绝对必要时,才应使课程流畅。否则,它们应尽可能不变。
DeadMG

25
@gnat:您在这里对PSE的每个问题和每个答案的质量都提出了质疑,但做得很棒,但是有时您恕我直言有点过于简洁。
Doc Brown

9
如果它们可以是有效的可选选项,则没有理由让他屈居第一。
DeadMG

75

DeadMG提供的答案几乎可以解决这个问题,但让我用不同的措词来表达:避免使对象具有无效状态。对象在完成任务的上下文中应该是“整个”的。否则它不应该存在。

因此,如果您需要流动性,请使用诸如Builder Pattern之类的东西。或与“真实事物”相反的其他任何东西,它是对构造中对象的单独验证,在后者中,保证后者具有有效状态,并且实际上是公开针对该状态定义的操作(例如getCompleteName)的事物。


7
So if you want fluidity, use something like the builder pattern-流动性或不变性(除非这就是您的意思)。如果要创建不可变的对象,但是需要推迟设置一些值,那么遵循的模式是在构建器对象上一个一个地设置它们,并且只有在我们收集了正确值所需的所有值之后才创建一个不可变的对象。状态...
Konrad Morawski 2014年

1
@KonradMorawski:我很困惑。您是在澄清还是自相矛盾?;)
back2dos

3
我试图对其进行补充...
Konrad Morawski 2014年

40

没有具有无效状态的对象。

构造器或构造器的目的是将对象的状态设置为一致且可用的状态。没有这种保证,对象中初始化状态不正确的问题就会出现。如果您要处理并发,并且在完全设置对象之前,某些对象可以访问该对象,则此问题会变得更加复杂。

因此,这部分的问题是“为什么要允许名称或姓氏为空?” 如果那不是使类正常工作的有效方法,请不要允许它。有一个可以正确验证对象创建的构造函数或生成器,如果不正确,请在那时提出问题。您可能还希望使用现有的@NotNull注释之一来帮助传达“此不能为空”并在编码和分析中强制执行。

有了此保证,就可以更轻松地对代码进行推理,做什么,而不必在奇数处引发异常或对getter函数进行过多检查。

获得更多功能的吸气剂。

关于这个问题有很多地方。您已经在Getters中获得了多少逻辑,并且在getters和setters中应该允许什么?从这里开始,getter和setter在Stack Overflow上执行其他逻辑。这是在类设计中一次又一次出现的问题。

其核心来自:

通常,如果未设置它们的值,则不应该将它们都抛出异常

你是对的。他们不应该。他们应该向世界其他地方“哑巴”,并尽其所能。在其中放置过多的逻辑会导致违反最小惊讶法则的问题。顺便说一句,您真的不想用a来包装吸气剂,try catch因为我们知道总体上我们有多喜欢这样做。

在某些情况下,您必须使用getFooJavaBeans框架之类的方法,并且在EL调用中有一些东西期望获得一个bean(因此<% bar.foo %>实际上是getFoo()在类中调用-撇开'bean应该做组合还是应该留给视图?”,因为一个人很容易想到其中一个显然可以是正确答案的情况)

还应意识到,有可能在接口中指定给定的getter,或者使其成为要重构的类的先前公开的公共API的一部分(以前的版本只是返回了“ completeName”,并且重构中断了)它分为两个字段)。

在一天结束时...

做最容易推理的事情。与设计代码相比,您将花费更多的时间维护代码(尽管花费的设计时间越少,维护的时间就越多)。理想的选择是以一种不会花费太多时间来维护的方式进行设计,但是也不要坐在那里几天思考它-除非这确实是一个设计选择,它将在几天后产生影响。 。

试图坚持使用吸气剂的语义纯正性private T foo; T getFoo() { return foo; }将使您在某个时候遇到麻烦。有时,代码只是不适合该模型,而试图将其保持为这种方式所要经过的扭曲是没有意义的,最终使设计变得更加困难。

有时,请接受您无法在代码中实现设计理想的想法。准则是准则-并非束缚。


2
显然,我的示例只是通过推理来改进编码样式的借口,而不是阻塞问题。但是我对您和其他人似乎对构造函数的重要性感到困惑,从理论上讲,他们信守诺言。实际上,通过继承来维护它们变得很麻烦,如果您从类中提取接口,则它们将不可用,如果我没出错,则不会被javabeans和诸如spring之类的其他框架所采用。在“阶级”思想的保护下,生活着许多不同类别的物体。值对象很可能具有验证构造函数,但这只是一种类型。
AgostinoX 2014年

2
能够推理代码是能够使用API​​编写代码或维护代码的关键。如果一个类具有允许其处于无效状态的构造函数,则将使思考“好吧,这是怎么做的” 变得更加困难。因为现在您必须考虑的情况比已考虑或计划的类的设计师更多。这种额外的概念负担会带来更多错误和更长的代码编写时间。另一方面,如果您可以查看代码并说“名称和姓氏永远不会为空”,则使用它们编写代码将变得更加容易。

2
不完整并不一定意味着无效。没有收据的发票可能正在处理中,发票提供的公共API将反映出这是有效状态。如果该对象的有效状态为null surnamename为null,则对其进行相应处理。但是,如果它不是有效状态-导致类中的方法引发异常,则应从初始化状态开始防止它处于该状态。您可以传递不完整的对象,但是传递无效的对象是有问题的。如果一个空姓是特殊的,请尽早阻止它。

2
相关博客文章为:设计对象初始化,并注意初始化实例变量的四种方法。其中之一是允许它处于无效状态,尽管请注意它会引发异常。如果您试图避免这种情况,那么该方法不是有效方法,您将需要使用其他方法-涉及始终确保有效对象。来自博客:“具有无效状态的对象比那些始终有效的对象更难使用和理解。” 这是关键。

5
“做最容易推理的事情。”那。

5

我不是Java专家,但这似乎符合您提出的两个约束。

getCompleteName如果名称未初始化,则不会引发异常,并且displayCompleteNameOnInvoice不会。

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}

扩展为什么这是一个正确的解决方案:通过这种方法,的调用方getCompleteName()不必关心属性是实际存储的还是动态计算的。
Bart van Ingen Schenau 2014年

18
要扩展此解决方案为何疯狂的原因,您必须在每次调用时检查是否为空getCompleteName,这很可怕。
DeadMG

不,@ DeadMG,只有在getCompleteName返回null时应引发异常的情况下,才需要检查它。应该是这样。此解决方案是正确的。
达伍德·伊本·卡里姆

1
@DeadMG是的,但是我们不知道getCompleteName()其他人在班级的其余部分甚至程序的其他部分使用的是什么。在某些情况下,null可能恰好需要返回。但是,尽管有一个方法的规范是“在这种情况下引发异常”,但这正是我们应该编写的代码。
达伍德·伊本·卡里姆

4
在某些情况下,这是最佳解决方案,但我无法想象。在大多数情况下,这似乎过于复杂:一种方法抛出不完整的数据,另一种方法返回null。恐怕呼叫者可能会错误地使用它。
sleske 2014年


1

您将整个检查名称的内容颠倒了。

getCompleteName()不必知道哪些功能会利用它。因此,不具有namewhen呼叫displayCompleteNameOnInvoice()不是getCompleteName()责任。

但是,这name是的明确前提displayCompleteNameOnInvoice()。因此,它应该承担检查状态的责任(或者,如果愿意,应该检查的责任委托给另一种方法)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.