对与错,我目前认为我应该始终尝试使我的代码尽可能健壮,即使这意味着添加我知道现在将无用的多余代码/检查,例如,我目前正在开发具有以下代码的移动应用程序:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
    }

    //2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
    if(rows.Count == 0)
    {
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}


专门看第二节,我知道如果第一节是正确的,那么第二节也是正确的。我无法想到为什么第一节为假而第二节为真,这使第二个if语句变得多余。

但是,将来可能会出现这种情况实际上,由于已知的原因,实际上需要第二个if语句。但是我知道在某些情况下此类代码对我来说是“隐藏的”错误。这意味着花了我更多的时间才能弄清楚为什么函数xyz在实际上应该执行abc的情况下执行def。 ,通过新的行为来增强代码更加容易,因为我不必回头并确保所有相关检查都已到位。这种代码?
(我也想知道这是好事还是坏事?) dDR是一个假设,假设没有截止日期。

TLDR:我是否应该进一步添加冗余代码,以使其在将来可能变得更健壮?

评论

可能重复设计以用于将来的更改或解决手头的问题

在软件开发中,永远都不会发生的事情。

如果您知道if(rows.Count == 0)永远不会发生,那么您可以在发生这种情况时引发异常-然后检查您的假设为何出错。

与问题无关,但我怀疑您的代码有错误。当row为null时,将创建一个新列表,并且(我猜)被丢弃。但是,当行不为空时,将更改现有列表。更好的设计可能是坚持要求客户端传递的列表可以为空也可以不为空。

为什么行会为空?至少在.NET中,没有充分的理由使集合为空。可以,但不能为空。如果rows为null,我会抛出一个异常,因为这意味着调用者在逻辑上有所失误。

#1 楼

作为练习,首先让我们验证您的逻辑。虽然我们会看到,但是您遇到的问题比任何逻辑问题都要大。 >

专门看第二节,我知道如果第一节是正确的,那么第二节也将是正确的。


即:A表示B,或更基本地讲(NOT A) OR B

然后:


I无法想到为什么第一节为假而第二节为真,这使得第二条if语句变得多余。


即:NOT((NOT A) AND B)。应用Demorgan定律得到(NOT B) OR A,它是B表示A。

因此,是的,检查是多余的。您在程序中似乎有四个代码路径,但实际上只有两个。

所以现在的问题是:如何编写代码?真正的问题是:该方法规定的合同是什么?条件是否多余的问题是一个红鲱鱼。真正的问题是“我设计了合理的合同,我的方法是否清楚地执行了该合同?”

让我们看一下声明: br />它是公共的,因此它必须对来自任意调用者的不良数据具有鲁棒性。 />
,但该方法的名称是动词,表明它的副作用非常有用。 >
空列表是可以的
其中包含一个或多个元素的列表是可以的
其中没有元素的列表是错误的,应该是不可能的。

这份合同太疯狂了。想象一下为此编写文档!想象一下编写测试用例!

我的建议:重新开始。这个API上面写满了糖果机接口。 (该表达来自微软的糖果机的一个古老故事,价格和选择项都是两位数,输入“ 75”即项目75的价格非常容易,您会得到有趣的事实:是的,当我试图从Microsoft的自动售货机中取出胶时,我实际上是错误地做到了! />
使您的方法对副作用或返回值有用,而不对两者都有好处。

不接受可变类型作为输入,例如列表;如果需要一系列信息,请使用IEnumerable。只读取序列;除非很清楚这是方法的约定,否则不要写传入的集合。通过使用IEnumerable,您会向调用方发送一条消息,告知您不打算对其集合进行突变。空序列是可憎的。如果有必要,要求调用者传递一个空序列,永远不要为null。

如果呼叫者违反了您的合同,立即崩溃,告诉他们您的意思是生意,以便他们抓住测试中的错误,而不是生产中的错误。

首先设计合同尽可能地明智,然后清楚地执行合同。这就是使设计过时的方法。因此,这里有一些附加的一般建议:


如果存在一个事实,即您作为开发人员可以推断但编译器无法推断,则可以使用断言来记录该事实。如果另一个开发人员(例如将来的您或您的一个同事)违反了该假设,则断言将告诉您。
获取测试覆盖率工具。确保测试覆盖代码的每一行。如果存在未发现的代码,则说明您缺少测试或代码无效。死代码是非常危险的,因为通常不希望死代码!数年前令人难以置信的可怕的Apple“ goto fail”安全漏洞马上浮现在脑海。
获取静态分析工具。哎呀,拿几个;每个工具都有其特定的专业,没有一个是其他工具的超集。当它告诉您存在无法访问或冗余的代码时,请注意。再次,这些可能都是错误。我就是这么说的做这些事情将使应对未来变得更加容易。未来最困难的部分是处理人们过去编写的所有有问题的糖果机代码。立即解决,未来成本会降低。

评论


我以前从来没有真正考虑过这样的方法,现在想起来我似乎总是想尽一切办法,而实际上,如果调用我的方法的人/方法没有通过我所需要的,那我就不应该不要试图纠正他们的错误(当我实际上不知道他们的意图时),我应该崩溃。感谢您,已经学到了宝贵的一课!

–孩子代码
16 Mar 28 '16 at 10:34

方法返回值的事实并不意味着该方法也没有副作用。在并发代码中,函数既要返回又必须返回(想象没有这样的能力的CompareExchange!),甚至在非并发情况下,诸如“添加一条记录,如果不存在则返回一条记录,然后返回传递的记录,比任何既不使用副作用又不使用返回值的方法更方便。

–超级猫
16 Mar 28 '16 at 16:04

@KidCode是的,Eric非常擅长清晰地解释事物,甚至包括一些非常复杂的主题。 :)

–梅森·惠勒
16-3-28在21:29

@supercat可以,但是也很难推理。首先,您可能希望查看不会修改全局状态的内容,从而避免并发问题和状态损坏。当这不合理时,您仍将两者隔离开来-可以使您清楚地了解并发性是一个问题(因此被认为是特别危险的)和处理的地方。这是原始OOP文件的核心思想之一,也是参与者有用性的核心。没有宗教规则-仅在合理的情况下才希望将两者分开。通常是这样。

–罗安
16-3-29在9:02

这对几乎每个人来说都是非常有用的信息!

– Adrian Buzea
16 Mar 30 '16 at 7:29

#2 楼

您在上面显示的代码中所做的只是防御性的编码,而不仅仅是对未来的证明。两者都是适合您的需求的测试。

第1节测试并更正了if对象。旁注:创建列表不会创建任何子项(例如null)。

第2部分测试并纠正了用户和/或实现错误。仅仅因为您有一个CalendarRow并不意味着您在列表中有任何项目。用户和实现者将执行您无法想象的事情,这仅仅是因为他们被允许这样做,无论您是否有意义。

评论


实际上,我正在采用“愚蠢的用户技巧”来表示输入。是的,您永远不应信任输入。即使是在课堂之外。验证!如果您认为这只是未来的问题,今天很乐意向您介绍。

–candied_orange
16 Mar 27 '16 at 20:03

@CandiedOrange,这是本意,但其措辞并未传达企图的幽默。我改变了措辞。

–亚当·扎克曼(Adam Zuckerman)
16 Mar 27 '16 at 20:09

这里只是一个简单的问题,如果这是实现错误/错误数据,我是否应该崩溃,而不是尝试修复其错误?

–孩子代码
16 Mar 28 '16 at 10:38

@KidCode每当您尝试恢复“修复其错误”时,都需要做两件事,返回到已知的良好状态,并且不要悄悄地失去宝贵的输入。在这种情况下,遵循该规则会产生一个问题:零行列表是否有价值?

–candied_orange
16-3-28在11:56

强烈不同意此答案。如果一个函数接收到无效的输入,则从定义上来说意味着程序中存在错误。正确的方法是对无效输入抛出异常,以便您发现问题并修复错误。您描述的方法只是隐藏错误,这使它们更隐蔽并且更难追踪。防御性编码意味着您不会自动信任输入,而是对其进行验证,但这并不意味着您应该随机猜测才能“修复”无效或意外的输入。

–雅克B
16 Mar 30 '16 at 9:02

#3 楼

我想,这个问题基本上是没有道理的。是的,编写健壮的代码是一个好主意,但是您的示例中的代码略微违反了KISS原则(因为很多此类“未来证明”代码都是这样)。就个人而言,将来可能不会费心编写防弹代码。我不知道未来,因此,当未来到来时,任何此类“未来防弹”代码注定都会惨遭失败。您使用assert()宏或类似工具做出的明确假设。这样,当未来到来之时,它将准确地告诉您您的假设不再适用的地方。

评论


我喜欢您关于不知道未来会怎样的观点。现在,我真正要做的只是猜测可能存在的问题,然后再次猜测解决方案。

–孩子代码
16 Mar 27 '16 at 18:12

@KidCode:很好的观察。实际上,您自己的想法比这里的许多答案(包括您已经接受的答案)要聪明得多。

–雅克B
16 Mar 30 '16 at 15:49

我喜欢这个答案。将代码保持在最小限度,以便将来的读者轻松理解为什么需要进行任何检查。如果将来的读者看到检查中看起来不必要的东西,他们可能会浪费时间试图理解检查的原因。未来的人可能会修改此类,而不是其他使用它的人。另外,请勿编写无法调试的代码,如果您尝试处理当前无法发生的情况,情况就是如此。 (除非编写的单元测试使用主程序不执行的代码路径。)

– Peter Cordes
16-3-31在2:39



#4 楼

您可能要考虑的另一个原则是快速失败的想法。这样的想法是,当程序出现问题时,您希望立即完全停止该程序,至少在开发它的过程中,然后再发布它。在此原则下,您想编写大量检查以确保您的假设成立,但请认真考虑让程序在违反假设的情况下停止在其轨道上运行。

大胆地说,如果甚至程序中的一个小错误,您都希望它在观看时完全崩溃!如果您正在编写一段代码,并且认为它已经完成了,但是在测试时它崩溃了,那么毫无疑问您还没有完成。此外,大多数编程语言为您提供了出色的调试工具,这些程序在程序完全崩溃时最容易使用,而不是在出现错误后尽力而为。最大,最常见的示例是,如果您通过引发未处理的异常而使程序崩溃,则异常消息将告诉您有关该错误的大量信息,包括哪一行代码失败以及该程序采用的代码路径它到那行代码的方式(堆栈跟踪)。

有关更多想法,请阅读此短文:不要将程序以垂直位置钉住
/>这与您有关,因为有时候您可能正在写检查,因为您希望程序即使在出现问题后也可以尝试继续运行。例如,考虑一下斐波那契数列的这种简短实现:那就行不通了!因此,您需要添加检查以确保使用非负输入调用该函数。

编写这样的函数可能会很诱人:没意识到!更糟糕的是,您的程序可能会继续运行,但会开始产生荒谬的结果,而不会向您提供任何错误信息。这些是最难修复的错误类型。

相反,最好编写如下检查:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}


现在如果不小心以负输入调用Fibonacci函数,您的程序将立即停止运行,并告知您有问题。此外,通过为您提供堆栈跟踪,该程序将使您知道程序的哪一部分试图错误地执行Fibonacci函数,从而为调试错误提供了一个很好的起点。

评论


C#是否没有特定种类的异常来指示无效的参数或超出范围的参数?

–JDługosz
16 Mar 28 '16在8:06

@JDługosz是的! C#具有ArgumentException,而Java具有IllegalArgumentException。

–凯文
16 Mar 28 '16 at 10:30

问题是使用c#。为了完整性,这里是C ++(链接到摘要)。

–JDługosz
16-3-28在13:59

“快速失败到最近的安全状态”更为合理。如果您将应用程序制作为在出现意外情况时崩溃,则可能会丢失用户的数据。用户数据处在危险之中的地方是“倒数第二个”异常处理的好地方(在某些情况下,您只能举起手来什么都做,这是最终崩溃)。只有在调试时崩溃才打开另一蠕虫-无论如何,您都需要测试要部署到用户的内容,现在您将一半的测试时间花在了用户永远不会看到的版本上。

–罗安
16 Mar 29 '16 at 8:52

@Luaan:但是,这不是示例函数的责任。

–whatsisname
16-3-29在23:26

#5 楼

您是否应该添加冗余代码?不。

但是,您描述的不是冗余代码。无论您执行此操作,还是只是让用户阅读文档并自己避免违规,都是完全主观的。要谨慎。以C ++的std::vector::operator[]为例。暂时将VS的调试模式实现搁置一旁,该函数不会执行边界检查。如果您请求的元素不存在,则结果是不确定的。由用户决定是否提供有效的向量索引。这是非常故意的:您可以通过在调用站点上添加边界选择来“选择加入”,但是如果operator[]实现要执行它,那么您将不能“选择退出”。

但是,如果您要为某个更高级别的接口编写AddEmployee(string name)函数,我完全希望该函数至少会引发异常。提供了一个空的name,并且此前提条件已在函数声明上方立即记录。您可能不会在今天为此功能提供未经验证的用户输入,但是以这种方式使其“安全”意味着可以容易地诊断出将来出现的任何先决条件违规,而不是潜在地触发一系列难以理解的多米诺骨牌检测错误。这不是多余的:是勤奋的。

如果我不得不提出一个通用规则(尽管作为通用规则,我尽量避免使用那些通用规则),我会说一个满足条件的函数以下任何一项:


使用超高级语言(例如JavaScript而不是C)
位于接口边界
不是性能-critical
直接接受用户输入

……可以从防御性编程中受益。在其他情况下,您仍然可以编写在测试过程中触发但在发行版本中被禁用的assert离子,以进一步提高发现错误的能力。

该主题在Wikipedia(https:// zh.wikipedia.org/wiki/Defensive_programming)。

#6 楼

十个编程指令中的两个与此处相关:


您不应假定输入正确无误
您不得编写代码供将来使用

在这里,检查空值不是“编写代码以备将来使用”。编写代码以供将来使用就像添加接口一样,因为您认为它们可能在“某天”有用。换句话说,命令是不要添加抽象层,除非现在需要它们。

检查null与将来的使用无关。它与命令1有关:不要假设输入正确。永远不要假设您的函数会收到一些输入子集。无论输入多么虚假和混乱,函数都应该以逻辑方式进行响应。

评论


这些编程诫命在哪里。你有链接吗?我很好奇,因为我参与了一些遵守第二条诫命的程序,而有些则没有。始终遵循命令的人,尽管命令具有直觉上的逻辑,也不得编写代码以备将来使用,但会遇到可维护性问题。我发现,在现实生活的编码中,该命令仅在控制功能列表和截止日期并确保您不需要将来使用的代码才能到达它们的代码中有效……也就是说,永远不会。

–Cort Ammon
16-3-28在14:50

可证明的:如果可以估计未来使用的可能性以及“未来使用”代码的预计价值,并且这两者的乘积大于添加“未来使用”代码的成本,则从统计学上讲,这是最优的添加代码。我认为,在开发人员(或管理人员)被迫承认他们的估算技能不如他们希望的那样可靠的情况下,就会出现命令,因此,作为防御措施,他们只是选择根本不估算未来的任务。

–Cort Ammon
16 Mar 28 '16 at 14:52

@CortAmmon在编程中没有宗教诫命的地方-“最佳实践”仅在上下文中才有意义,而没有推理的“最佳实践”学习会使您无法适应。我发现YAGNI非常有用,但这并不意味着我不认为以后添加扩展点会很昂贵的地方-这只是意味着我不必提前考虑简单的情况。当然,随着时间的流逝,这种情况也会改变,因为越来越多的假设被添加到您的代码中,从而有效地增加了代码的界面-这是一种平衡。

–罗安
16 Mar 29 '16 at 8:44

@CortAmmon您的“可证明的”案例忽略了两个非常重要的成本-估计本身的成本以及(可能不必要的)扩展点的维护成本。人们低估了估算值,从而得出了极其不可靠的估算值。对于一个非常简单的功能,思考几秒钟可能就足够了,但是很可能您会发现一整套最初的“简单”功能之后的蠕虫。沟通是关键-随着事情的发展,您需要与领导者/客户交谈。

–罗安
16 Mar 29 '16 at 8:49

@Luaan我试图为您辩护,在编程中没有宗教戒律的地方。只要存在一个业务案例,其中扩展点的估计和维护成本就受到足够的限制,那么上述“命令”就有问题。根据我在代码方面的经验,是否要离开这样的扩展点这个问题从未很好地适合一种方式或另一种方式。

–Cort Ammon
16 Mar 29 '16 at 14:28

#7 楼

“冗余代码”和“ YAGNI”的定义通常取决于我对您的展望。

如果遇到问题,那么您倾向于以避免这种问题的方式编写将来的代码。另一个没有遇到过该特定问题的程序员可能会认为您的代码是多余的。同行们正在以比您更快的速度推出功能,然后降低它。你了。

#8 楼

记录有关参数的所有假设是一个好主意。检查您的客户端代码是否不违反这些假设是一个好主意。我会这样做:但这是另一天的辩论。]

为什么这比您写的要好?如果在将来客户更改的情况下,当客户传入一个空列表时,您的代码有意义,那么正确的做法是添加第一行并将应用添加到其约会中。但是您怎么知道会发生这种情况?最好现在对未来做出更少的假设。

#9 楼

估计现在添加该代码的成本。它会相对便宜,因为它在您脑海中都是新鲜的,因此您将能够快速完成此操作。添加单元测试是必要的-没什么比一年后使用某种方法更糟糕的了,它没有用,而且您发现它从一开始就被破坏了并且从未真正起作用。

估计需要时添加该代码的成本。它将更加昂贵,因为您必须返回代码,记住所有内容,并且要困难得多。

估计实际需要附加代码的可能性。然后做数学。

另一方面,充满假设“ X将永远不会发生”的代码对于调试来说很糟糕。如果某些事情没有按预期工作,则意味着愚蠢的错误或错误的假设。您的“ X永远不会发生”是一种假设,并且在存在错误的情况下也是可疑的。这迫使下一个开发人员在上面浪费时间。通常最好不要依赖任何这样的假设。

评论


在第一段中,您忘记了随着时间的推移维护该代码的成本,事实证明,实际需要的功能与不必要添加的功能是互斥的。 。 。

–ruakh
16 Mar 28 '16 at 5:51

您还需要估计可能会潜入程序中的错误的成本,因为不会因输入无效而失败。但是您无法估算错误的成本,因为按照定义,它们是意外的。因此,整个“算术”都分崩离析。

–雅克B
16 Mar 30 '16 at 8:18

#10 楼

这里的主要问题是“如果做/不做会怎样?”?

正如其他人指出的那样,这种防御性编程是好的,但有时也很危险。

例如,如果您提供默认值,那么您将保持程序运行。但是该程序现在可能没有执行您想要的操作。例如,如果将空白数组写入文件,您现在可能已将错误从“崩溃,因为我意外提供了空值”变为“清除了日历行,因为我意外提供了空值”。 (例如,如果您开始删除该部分中未显示为“
// blah”的列表中的内容)

对我来说,关键是永不破坏数据。让我重复一遍;决不。腐败。数据。如果程序例外,您将得到一个错误报告,您可以对其进行补丁;如果将不良数据写入文件中,以后再用,则必须撒盐。

#11 楼

您在这里处理的基本上是一个接口。通过添加行为“当输入为null时,初始化输入”,您已经有效地扩展了方法接口-现在,它不再总是对有效列表进行操作,而是使其“固定”了输入。无论是接口的官方部分还是非正式的部分,您都可以打赌有人(很可能包括您在内)将使用此行为。特别是在类似public static方法的情况下。私有方法(尤其是私有实例方法)有一些余地。通过隐式扩展接口,您实际上使代码变得更加复杂。现在,假设您实际上并不想使用该代码路径-因此可以避免使用它。现在,您已经有了一些未经测试的代码,它们看起来像是方法行为的一部分。我现在可以告诉您,它可能有一个错误:当您传递列表时,该列表会被方法所突变。但是,如果没有,则创建一个本地列表,然后将其丢弃。这种不一致的行为会使您在半年内哭泣,因为您尝试查找一个晦涩的错误。

一般来说,防御性编程是非常有用的事情。但是,像其他任何代码一样,必须对防御性检查的代码路径进行测试。在这种情况下,他们会毫无理由地使您的代码复杂化,而我会选择这样的替代方法: rows为null的输入,并且您想使所有调用者尽快看到错误。

开发软件时,您需要兼顾许多价值。甚至健壮性本身也是一种非常复杂的质量-例如,我认为您的防御性检查不会比抛出异常健壮。为您提供一个从安全的地方重试的安全地方,异常非常方便-数据损坏问题通常比尽早识别并安全处理问题要难得多。最后,它们只会给您带来健壮的错觉,然后一个月后,您注意到约会的十分之一都消失了,因为您从未注意到更新过其他​​列表。哦。

请务必区分两者。防御性编程是一种在错误最相关的地方捕获错误的有用技术,可以极大地帮助您进行调试,并具有良好的异常处理能力,可以防止“ s亵腐败”。早期失败,快速失败。另一方面,您正在做的事情更像是“隐藏错误”-您正在杂耍输入并假设调用方的含义。这对于面向用户的代码(例如,拼写检查)非常重要,但是在面向开发人员的代码中看到此代码时应格外小心。主要的问题是,无论您进行哪种抽象,都会泄漏(“我想输入ofre的名称,而不是Fore!需要维护和理解,以及需要测试的代码。将确保通过非空列表的努力与修复一年后在生产中遇到的错误进行比较,这不是一个很好的折衷方案。在理想的世界中,您希望每种方法都专用于其自己的输入,返回结果并且不修改全局状态。当然,在现实世界中,您会发现很多情况都不是最简单,最清晰的解决方案(例如,保存文件时),但是我发现当没有理由让它们读取或操作时,保持方法“纯净”全局状态使代码更容易推理。相反,它也趋向于为您提供更自然的方法拆分方法:)

这并不意味着相反的所有意外都会使应用程序崩溃。如果您很好地使用了异常,则它们自然会形成安全的错误处理点,您可以在其中恢复稳定的应用程序状态,并允许用户继续其所做的工作(理想的是避免用户丢失任何数据)。在这些处理点处,您将看到解决问题(“找不到订单号2212。您是说2212b?”)或提供用户控制(“连接数据库时出错。重试?”)的机会。即使没有此类选项可用,至少也至少会给您带来没有损坏的机会-我已经开始欣赏使用usingtry ... finallytry ... catch更多的代码即使在特殊条件下也有很多机会保持不变性。

用户不应丢失其数据和工作。这仍然必须与开发成本等保持平衡,但这是一个很好的通用准则(如果用户决定是否购买您的软件-内部软件通常没有那么奢侈的话)。如果用户可以重新启动并返回到正在执行的操作,那么即使整个应用程序崩溃也不会成为问题。这是真正的健壮性-Word始终保存您的工作而不会损坏磁盘上的文档,并为您提供了在崩溃后重新启动Word之后还原这些更改的选项。首先它比没有错误要好吗?可能不会-尽管不要忘记,花在捕获一个罕见的bug上的工作可能在任何地方都更好。但这比替代方案要好得多-例如磁盘上已损坏的文档,自上次保存以来的所有工作都丢失了,文档自动替换为崩溃之前发生的更改,这些更改恰好是Ctrl + A和Delete。

#12 楼

我将基于您的假设来回答这个问题,即健壮的代码将从现在开始“使您受益”。如果长期利益是您的目标,那么我将设计和可维护性放在坚固性之上。

设计和坚固性之间的权衡是时间和重点。大多数开发人员宁愿拥有一套精心设计的代码,即使这意味着要经历一些麻烦点并进行一些附加条件或错误处理。使用几年后,用户可能已经确定了您真正需要的位置。

假设设计具有相同的质量,更少的代码更易于维护。这并不意味着如果您让已知问题持续数年,我们的生活就会更好,但是添加您不需要的东西会很困难。我们都看过遗留代码,发现不必要的部分。您必须拥有已经使用了多年的高水平的置信度更改代码。错误,找到比添加不需要的代码更好的事情。这是对所有其他长时间致力于无意义功能的其他开发人员的尊重。

#13 楼

不,你不应该。当您声明这种编码方式可能会隐藏错误时,您实际上是在回答自己的问题。这不会使代码更健壮-而是更易于出现错误并增加调试难度。至少一项。因此,问题是:编写代码以额外处理rows包含零个项目的第三个意外情况是一个好主意吗?在意外输入的情况下,您应始终抛出异常。考虑一下:如果代码的其他某些部分违反了您对方法的期望(即合同),则意味着存在错误。如果您想尽快发现某个错误,则可以对其进行修复,而异常可以帮助您做到这一点。

当前,代码正在猜测如何从代码中可能存在或可能不存在的错误中恢复。但是,即使存在错误,您也无法知道如何从中完全恢复。根据定义,错误具有未知的后果。也许某些初始化代码未按预期运行,可能会导致其他后果,而不仅仅是丢失的行。

因此,您的代码应如下所示: >
注意:在某些特定情况下,可以“猜测”如何处理无效输入,而不仅仅是抛出异常。例如,如果您处理外部输入,则无法控制。 Web浏览器是一个臭名昭著的示例,因为它们试图优雅地处理任何形式的格式错误和无效的输入。但这仅对外部输入有意义,而对程序其他部分的调用无效。


编辑:其他一些答案表明您正在执行防御性编程。我不同意。防御性编程意味着您不会自动相信输入是有效的。因此,参数验证(如上所述)是一种防御性编程技术,但这并不意味着您应该通过猜测来更改意外或无效的输入。健壮的防御方法是先验证输入,然后在输入意外或无效的情况下抛出异常。

#14 楼


现在是否应该添加冗余代码,以防将来将来需要?


您不应该随时添加冗余代码。

您不应添加将来只需要的代码。

无论发生什么情况,都应确保代码表现良好。

“行为良好”的定义”由您决定。我喜欢使用的一种技术是“偏执狂”异常。如果我100%确信某个情况永远不会发生,我仍然会编写一个异常,但是我这样做的方式是:a)清楚地告诉每个人我从未期望过这种情况,b)清楚地显示和记录因此伪造的伪代码如下:

伪代码示例:

我总是可以打开,写入或关闭文件,即,我不会进行详尽的错误处理。但是,如果(否:何时)我无法打开文件,则我的程序仍然会以受控方式失败。

用户界面当然不会向用户显示此类消息,它们将与堆栈跟踪一起在内部记录。同样,这些是内部的“ Paranoia”异常,它们只是确保在意外情况发生时代码“停止”。这个示例有些人为的设计,在实践中,我当然会在打开文件时对错误进行真正的错误处理,因为这经常发生(错误的文件名,USB只读安装的只读存储器,等等)。

如其他答案中所述,一个非常重要的相关搜索词将是“快速失败”,这对于创建健壮的软件非常有用。

#15 楼

这里有很多过于复杂的答案。您可能会问这个问题,因为您对那个片段代码感觉不正确,但是不确定为什么或如何修复它。所以我的回答是,问题很可能出在代码结构中(一如既往)。

首先,方法标头:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)


将约会分配到哪一行?这应该立即从参数列表中清除。在没有任何进一步知识的情况下,我希望方法参数看起来像这样:(Appointment app, CalendarRow row)

接下来,“输入检查”: >这是胡扯。


检查)方法调用者应确保它没有在方法内部传递未初始化的值。
检查)如果我不考虑将rows传递到方法中可能是错误的(请参见上面的评论),则它不应该由程序员负责。一种称为AssignAppointmentToRow的方法,除了以某种方式在某处分配约会以外,还可以通过其他任何方式来处理行。您的代码似乎包含(或至少尝试这样做)表示日历的显式数据结构(即List<CalendarRows> <-,如果您要这样做,应将其定义为Calendar,然后将Calendar calendar传递给您的方法)。如果您采用这种方式,我希望calendar会预先填充一些插槽,之后您可以在这些位置放置(分配)约会(例如,calendar[month][day] = appointment将是适当的代码)。但是,然后您也可以选择完全放弃主要逻辑中的日历结构,而仅使用List<Appointment>,其中Appointment对象包含属性date。然后,如果需要在GUI中的某个位置呈现日历,则可以在呈现之前创建此“显式日历”结构。

我不知道您的应用程序的详细信息,因此其中一些可能不适用于您,但是这两项检查(主要是第二项检查)都告诉我,代码中的关注点分离存在问题。

#16 楼

为简单起见,我们假设您最终将在N天(不晚或更早)内不需要这段代码。或者根本不需要。

伪代码:

let C_now   = cost of implementing the piece of code now
let C_later = ... later
let C_maint = cost of maintaining the piece of code one day
              (note that it can be negative)
let P_need  = probability that you will need the code after N days

if C_now + C_maint * N < P_need*C_later then implement the code else omit it.


C_maint的因子:


它是否在总体上改进了代码,使其更具自记录性,更易于测试?如果是,则期望C_maint为负数
它会使代码更大(因此更难阅读,更长的编译时间,执行测试等)吗?
是否有任何重构/重新设计的过程?如果是,则碰撞C_maint。这种情况下需要更复杂的公式,并且需要更多的时间变量,例如N。还可以提供有用的断言的东西,应该在3个月内实现50%的东西。

评论


您还需要考虑可能会渗入程序的错误的成本,因为您不会拒绝无效的输入。那么,您如何估算潜在的难以发现的错误的成本?

–雅克B
16 Mar 30 '16 at 8:23