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


114

对与错,我目前认为我应该始终尝试使我的代码尽可能地健壮,即使这意味着添加我知道现在将无用的冗余代码/检查,但是它们可能是下线的x年。

例如,我目前正在开发具有以下代码的移动应用程序:

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已知的原因,将来可能会有实际需要第二条语句的情况。

某些人可能一开始就看过这个,并认为我正在考虑未来,这显然是一件好事。但是我知道在某些情况下这种代码对我来说是“隐藏的”错误。这意味着我花了更长的时间才弄清楚函数xyzabc实际应该执行的时候为什么要执行def

另一方面,在很多情况下,这种代码使使用新行为增强代码变得容易得多,因为我不必回头并确保所有相关检查都已到位。

对于此类代码,是否有任何通用的经验法则指南?(我也想知道这是好事还是坏事?)

注意:这可以被认为与这个问题类似但是与那个问题不同,我想在没有截止日期的情况下回答。

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



95
在软件开发中,永远不会发生的事情总是发生。
罗曼·赖纳

34
如果您知道if(rows.Count == 0)永远不会发生,那么您可以在发生这种情况时提出一个例外-然后检查您的假设为什么出错了。
knut

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

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

Answers:


176

作为练习,首先让我们验证您的逻辑。尽管正如我们将看到的那样,您遇到的问题比任何逻辑问题都要大。

称为第一条件A和第二条件B。

您首先说:

专门研究第二部分,我知道如果第一部分是正确的,那么第二部分也将是正确的。

也就是说:A表示B,或更笼统地说 (NOT A) OR B

接着:

对于第一部分为假而第二部分为真的原因,我想不出任何原因,这使第二个if语句变得多余。

即:NOT((NOT A) AND B)。应用Demorgan定律得出(NOT B) OR AB表示A.

因此,如果您的两个陈述均正确,则A表示B,B表示A,这意味着它们必须相等。

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

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

让我们看一下声明:

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

它是公开的,因此必须对来自任意调用者的不良数据具有鲁棒性。

它返回一个值,因此它应该对返回值有用,而不是副作用。

但是该方法的名称是一个动词,表明该方法对于其副作用很有用。

list参数的协定为:

  • 空列表可以
  • 包含一个或多个元素的列表可以
  • 没有元素的列表是错误的,应该是不可能的。

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

我的建议:重新开始。这个API上面写满了糖果机接口。(该表达来自微软的糖果机的一个古老故事,其中价格和选择项都是两位数字,并且非常容易键入“ 85”,即项目75的价格,您会得到有趣的事实:是的,当我试图将口香糖从Microsoft的自动售货机中取出时,我实际上是错误地做到了!)

设计合理合同的方法如下:

使您的方法可用于其副作用或返回值,而不同时用于两者。

不要接受可变类型作为输入,例如列表;如果您需要一系列信息,请使用IEnumerable。只读取序列;除非清楚这是方法的约定,否则不要写传入的集合。通过使用IEnumerable,您会向调用方发送一条消息,告知您不会更改其集合。

永远不要接受空值;空序列是可憎的。要求调用者传递一个空序列(如果有意义),永远不要为null。

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

首先设计合同,使其尽可能合理,然后清楚地执行合同。 就是使设计过时的方法。

现在,我只讨论了您的具体情况,您问了一个一般性问题。因此,这里有一些其他一般建议:

  • 如果您作为开发人员可以推断出事实,而编译器则无法推断出事实,则可以使用断言来记录该事实。如果另一个开发人员(例如将来的您或您的一个同事)违反了该假设,则声明将告诉您。

  • 获取测试覆盖率工具。确保测试覆盖代码的每一行。如果存在未发现的代码,则说明您缺少测试或代码无效。死代码是非常危险的,因为通常它并不是要死的!数年前令人难以置信的可怕的Apple“ goto fail”安全漏洞马上浮现在脑海。

  • 获取静态分析工具。哎呀,拿几个;每个工具都有其特定的专业,没有一个是其他工具的超集。当它告诉您存在无法访问或冗余的代码时,请注意。同样,这些可能是错误。

如果听起来像我在说:首先,设计好代码,其次,测试一下代码,以确保它今天正确无误,这就是我要说的。做这些事情将使应对未来变得更加容易。未来最困难的部分是处理人们过去编写的所有有问题的糖果机代码。今天就做好,将来成本会降低。


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

4
方法返回值的事实并不意味着该方法也没有副作用。在并发代码中,函数既要返回它们的能力通常也是至关重要的(想象一下CompareExchange没有这种能力!),即使在非并发方案中,诸如“添加一条记录(如果不存在则返回一条记录,然后返回传入的一条记录)之类的东西”也是如此。记录或已存在的记录”比没有同时使用副作用和返回值的任何方法都更方便。
超级猫

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

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

5
这对几乎每个人来说都是非常有用的信息!
Adrian Buzea '16

89

在上面显示的代码中,您所做的并不是防御性的编码,而是对未来的证明。

这两个if语句测试不同的事物。两者都是适合您的需求的测试。

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

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


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

1
@CandiedOrange,这是本意,但其措辞并未传达企图的幽默。我改变了措辞。
亚当·祖克曼

3
这里只是一个简单的问题,如果这是实现错误/错误数据,我是否应该崩溃,而不是尝试解决他们的错误?
KidCode

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

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

35

我想,这个问题基本上是没有道理的。是的,编写健壮的代码是一个好主意,但是您的示例中的代码略微违反了KISS原则(因为很多此类“面向未来的”代码都是如此)。

我个人将来可能不会为代码防弹而烦恼。我不知道未来,因此,任何这样的“未来防弹”代码注定在未来到来时都会惨败。

相反,我更喜欢另一种方法:使用assert()宏或类似的工具进行明确的假设。这样,当未来到来之时,它将准确地告诉您您的假设不再适用的地方。


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

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

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

23

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

大胆地说,如果您的程序中甚至有一个小错误,您希望它在观看时完全崩溃!

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

要获得更多的想法,请阅读以下短文:不要将程序垂直放置


这与您有关,因为有时您正在编写的检查可能在那里,因为您希望程序即使在出现问题后也可以尝试继续运行。例如,考虑斐波纳契数列的这种简短实现:

// 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;
}

这有效,但是如果有人向您的函数传递了负数怎么办?那就行不通了!因此,您需要添加检查以确保使用非负输入调用该函数。

编写这样的函数可能很诱人:

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

    // Make sure the input is nonnegative
    if(n < 0) {
        n = 1; // Replace the negative input with an input that will work
    }

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

    return b;
}

但是,如果执行此操作,然后在以后意外用负输入调用Fibonacci函数,您将永远不会意识到这一点!更糟糕的是,您的程序可能会继续运行,但会开始产生荒谬的结果,而不会向您提供任何错误信息。这些是最难修复的错误类型。

相反,最好这样写一张支票:

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

    // Make sure the input is nonnegative
    if(n < 0) {
        throw new ArgumentException("Can't have negative inputs to Fibonacci");
    }

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

    return b;
}

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


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

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

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

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

2
@Luaan:但这不是示例函数的责任。
whatsisname 2016年

11

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

但是,您所描述的不是冗余代码

您所描述的是防御性编程,以防止违反函数先决条件的调用代码。是否执行此操作或仅让用户阅读文档并自己避免这些违规行为完全是主观的。

我个人是这种方法的忠实拥护者,但是对于所有方法,您都必须谨慎。以C ++为例std::vector::operator[]。暂时将VS的调试模式实现搁置一旁,此函数不会执行边界检查。如果您请求的元素不存在,则结果是不确定的。由用户决定是否提供有效的向量索引。这是非常有意的:您可以通过在调用站点上添加边界选择来“选择加入”,但是如果operator[]实现要执行,那么您将不能“选择退出”。作为一个相当低级的功能,这很有意义。

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

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

  • 生活在超高级语言中(例如,JavaScript而不是C)
  • 位于界面边界
  • 不是关键的性能
  • 直接接受用户输入

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

可以在Wikipedia(https://en.wikipedia.org/wiki/Defensive_programming)上进一步探讨该主题。


9

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

  • 您不得认为输入正确

  • 您不得编写代码以备将来使用

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

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


5
这些编程诫命在哪里。你有链接吗?我很好奇,因为我参与了一些遵守第二条诫命的程序,而有些则没有。不变的是,那些谁订阅了诫Thou shall not make code for future use跑进可维护性的问题越早,尽管戒律的直观的逻辑。我发现,在现实生活的编码中,该命令仅在控制功能列表和截止日期的代码中有效,并确保您不需要将来使用的代码即可到达它们……也就是说,永远不会。
Cort Ammon

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

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

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

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

7

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

如果遇到问题,则倾向于以避免这种问题的方式编写将来的代码。另一个没有遇到过特定问题的程序员可能会认为您的代码多余了。

我的建议是,如果负载过重并且您的同龄人以比您更快的速度推出功能,然后减少它,请跟踪您花了多少时间在“尚未出错的东西”上。

但是,如果您像我一样,我希望您已经将其全部键入为“默认”,并且它并没有真正吸引您。


6

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

/** ...
*   Precondition: rows is null or nonempty
*/
public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    Assert( rows==null || rows.Count > 0 )
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

[假设这是C#,Assert可能不是最佳的工具,因为它没有在发布的代码中编译。但这是另一天的辩论。]

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


5

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

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

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

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


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

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

3

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

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

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

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

您所有“不必要的”决定都应在此前提下做出。


2

您在这里处理的基本上是一个接口。通过添加行为“当输入为null初始化输入时”,您已经有效地扩展了方法接口-现在,它不再总是对有效列表进行操作,而是使其“固定”了输入。无论这是界面的官方部分还是非官方部分,您都可以打赌有人(很可能包括您在内)将使用此行为。

接口应该保持简单,并且应该相对稳定-尤其是在某种public static方法中。私有方法(尤其是私有实例方法)有一些余地。通过隐式扩展接口,您实际上使代码变得更加复杂。现在,假设您实际上并不使用该代码路径-因此可以避免使用它。现在你已经有了一个位是未经测试的代码假装喜欢它的方法的行为的一部分。我现在可以告诉您,它可能有一个错误:当您传递列表时,该列表会被方法所突变。但是,如果不这样做,则会创建一个本地列出,然后将其丢弃。这种不一致的行为会使您在半年内哭泣,因为您尝试查找一个隐蔽的错误。

通常,防御性编程是非常有用的事情。但是,像其他任何代码一样,必须对防御性检查的代码路径进行测试。在这种情况下,他们无缘无故地使您的代码复杂化,而我会选择这样的替代方法:

if (rows == null) throw new ArgumentNullException(nameof(rows));

你不希望当输入rows为空,你想使错误有目共睹,您的呼叫者尽快

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

确保区分两者。防御性编程是一种在错误最相关的地方捕获错误的有用技术,可以极大地帮助您进行调试,并具有良好的异常处理能力,可以防止“ s亵腐败”。早期失败,快速失败。另一方面,您正在做的事情更像是“隐藏错误”-您正在杂耍输入并假设调用方的含义。这对于面向用户的代码(例如,拼写检查)非常重要,但是在面向开发人员的代码中看到此代码时,请当心。

主要的问题是,无论您进行哪种抽象,都会泄漏(“我想输入ofre,而不要输入!愚蠢的拼写检查器!”),并且处理所有特殊情况和修正的代码仍然是您的代码需要维护和理解,以及需要测试的代码。将确保通过非空列表的努力与修复一年后在生产中遇到的错误进行比较,这不是一个很好的折衷方案。在理想的世界中,您希望每种方法都专用于其自己的输入,返回结果并且不修改全局状态。当然,在现实世界中,您会发现很多情况并非如此最简单,最清晰的解决方案(例如,保存文件时),但是我发现在没有理由让它们读取或操纵全局状态的情况下保持方法“纯净”会使代码更容易推理。它还倾向于为您提供更自然的方法拆分方法:)

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

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


1

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

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

假设设计具有相同的质量,则更少的代码更易于维护。这并不意味着如果您让已知问题持续数年,我们的生活就会更好,但是添加您不需要的东西会很困难。我们都看过遗留代码,发现不必要的部分。您必须拥有已经使用了多年的高层信任更改代码。

因此,如果您认为自己的应用程序设计合理,易于维护且没有任何错误,那么找到比添加不需要的代码更好的方法。这是对所有其他长时间致力于无意义功能的其他开发人员的尊重。


1

不,你不应该。当您声明这种编码方式可能会隐藏错误时,您实际上是在回答自己的问题。这不会使代码更健壮-而是会使它更容易出现错误并增加调试难度。

您陈述当前对rows参数的期望:它为null或否则至少具有一项。因此,问题是:编写代码以额外处理意外的第三种情况(rows零项)是个好主意吗?

答案是不。在意外输入的情况下,您应始终抛出异常。考虑一下:如果代码的其他某些部分违反了您对方法的期望(即合同),则意味着存在bug。如果有一个错误,您希望尽早知道它,以便您可以修复它,那么异常将帮助您做到这一点。

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

因此,您的代码应如下所示:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    if (rows != null && rows.Count == 0) throw new ArgumentException("Rows is empty."); 

    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

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


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


1

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

您不应该在任何时候添加冗余代码。

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

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

“行为良好”的定义取决于您的需求。我喜欢使用的一种技术是“偏执狂”异常。如果我100%确信某个情况永远不会发生,我仍然会编写一个异常,但是我这样做的方式是:a)清楚地告诉每个人我从未期望过这种情况发生,并且b)清楚地显示和记录并因此以后不会导致腐败蔓延。

伪代码示例:

file = File.open(">", "bla")  or raise "Paranoia: cannot open file 'bla'"

file.write("xytz") or raise "Paranoia: disk full?"

file.close()  or raise "Paranoia: huh?!?!?"

这清楚地表明,我100%确信可以始终打开,写入或关闭文件,即,我不会竭力创建详尽的错误处理。但是,如果(否:何时)我无法打开文件,则我的程序仍会以受控方式失败。

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

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


-1

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

一,方法头:

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

指定预约什么回事?这应该立即从参数列表中清除。在没有任何进一步知识的情况下,我希望方法参数看起来像这样:(Appointment app, CalendarRow row)

接下来,进行“输入检查”:

//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);
}

这是胡扯。

  1. 检查)方法调用者应确保它没有在方法内部传递未初始化的值。这是程序员的责任(尝试),不要愚蠢。
  2. 检查)如果我没有考虑到rows传入该方法可能是错误的(请参见上面的注释),那么除了AssignAppointmentToRow以某种方式将约会分配给某处之外,以其他任何方式操纵行的方法都不应该承担责任。

但是,在某个地方分配约会的整个概念很奇怪(除非这是代码的GUI部分)。您的代码似乎包含(或至少尝试包含)表示日历的显式数据结构(如果要以这种方式,List<CalendarRows>应将<-定义为Calendar某个位置,然后将其传递Calendar calendar给方法)。如果您采用这种方式,我希望会calendar在您随后放置(分配)约会的位置预先插入一些槽(例如calendar[month][day] = appointment,适当的代码)。但你也可以选择完全抛弃主逻辑日历结构,只是有List<Appointment>其中Appointment的对象包含属性date。然后,如果需要在GUI中的某个位置呈现日历,则可以在呈现之前创建此“显式日历”结构。

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


-2

为简单起见,让我们假设您最终将在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

这么大的事情只是权衡代码,可能仅在2年内很少用到,应该忽略掉,但还有一点点有用的断言,在3个月内需要50%的那件事,应该实施。


您还需要考虑可能会渗入程序的错误的成本,因为您不会拒绝无效的输入。那么,您如何估算潜在的难以发现的错误的成本呢?
JacquesB '16
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.