通过可能无用的异常处理来增强代码


12

以防万一代码的另一部分没有正确编码,是实现无用异常处理的好习惯吗?

基本例子

一个简单的,所以我不会让每个人都放松:)。

假设我正在编写一个应用程序,它将显示一个人的信息(姓名,地址等),数据是从数据库中提取的。假设我是UI部分的编码人员,而其他人正在编写DB查询代码。

现在,假设您的应用程序规范说明如果该人员的信息不完整(例如,数据库中缺少该名称),则对查询进行编码的人员应通过为缺失字段返回“ NA”来处理此问题。

如果查询的编码不正确怎么办?如果编写查询的人为您处理了不完整的结果,并且当您尝试显示信息时一切都崩溃了,因为您的代码不准备显示空的东西怎么办?

这个例子很基础。我相信你们大多数人会说“这不是您的问题,您不应对这次崩溃负责”。但是,仍然是崩溃的一部分。

另一个例子

假设现在我是编写查询的人。规范与上面的说法不同,但是在向数据库中添加人员时,编写“插入”查询的人应确保所有字段均完整,以避免插入不完整的信息。我应该保护我的“选择”查询以确保为UI家伙提供完整的信息吗?

问题

如果规范中没有明确指出“此人是负责处理这种情况的人”,该怎么办?如果第三方实现了另一个查询(类似于第一个查询,但在另一个DB上)并使用您的UI代码显示该查询,但在他的代码中不处理这种情况怎么办?

即使我不是应该处理这种情况的人,我也应该采取必要的措施来防止可能的崩溃吗?

我不是要寻找“他是崩溃的负责人”之类的答案,因为我不想在这里解决冲突,我想知道,如果我保护我的代码免受情况影响,这不是我的责任处理?在这里,一个简单的“如果空着做某事”就足够了。

通常,此问题解决了冗余异常处理。我之所以这样问,是因为当我一个人在一个项目上工作时,我可能会在连续的函数中编写2-3次类似异常处理的代码,以防万一我做错了什么,让一个糟糕的情况解决了。


4
您是在谈论“测试”,但是据我所知,您的意思是“在生产中应用的测试”,最好称为“验证”或“异常处理”。
布朗

1
是的,合适的词是“异常处理”。
rdurand

然后更改了错误的标签
Doc Brown,

我将您推荐给DailyWTF-确定要进行这种测试吗?
gbjbaanb

@gbjbaanb:如果我正确理解了您的链接,那根本不是我在说的。我不是在谈论“愚蠢的测试”,而是在谈论复制异常处理。
rdurand 2013年

Answers:


14

您在这里谈论的是信任边界。您是否信任应用程序和数据库之间的界限?数据库是否相信来自应用程序的数据总是经过预验证的?

这是每个应用程序都必须做出的决定,没有正确与错误的答案。我倾向于错误地将太多的边界称为信任边界,其他开发人员会乐于相信第三方API每次都可以做您期望他们做的事情。


5

鲁棒性原则“对您发送的内容保持保守,对您接受的内容保持自由”是您所追求的。这是一个很好的原则-编辑:只要它的应用程序不会隐藏任何严重的错误-但我同意@pdr,无论是否应应用它始终取决于情况。


有人认为“健壮性原则”就是废话。本文提供了一个示例。

@MattFenwick:感谢您指出这一点,这是一个有效的观点,我对答案做了一些修改。
布朗

2
这是一篇更好的文章,指出了“健壮性原则”的问题:joelonsoftware.com/items/2008/03/17.html
hakoja,2013年

1
@hakoja:说实话,我很了解本文,这是关于您开始遵循健壮性原理时遇到的问题(例如某些MS家伙尝试使用较新的IE版本)。但是,这与原始问题有点距离。
布朗

1
@DocBrown:这就是为什么您不应该对自己接受的东西持开放态度的原因。健壮并不意味着您需要毫无怨言地接受扔给您的所有东西,而只是需要接受而不会崩溃就接受的所有东西。
Marjan Venema

1

这取决于您要测试的内容。但让我们假设您的测试范围仅是您自己的代码。在这种情况下,您应该测试:

  • “高兴的情况”:为您的应用程序提供有效的输入,并确保它产生正确的输出。
  • 失败案例:向您的应用程序提供无效输入,并确保它正确处理了它们。

为此,您不能使用同事的组件:而是使用嘲讽,即用可以从测试框架控制的“假”模块替换应用程序的其余部分。具体执行方式取决于模块接口的方式。只需用硬编码的参数调用模块的方法就足够了,并且它可能变得复杂,就像编写将其他模块的公共接口与测试环境连接在一起的整个框架一样。

不过,那只是单元测试用例。您还需要集成测试,在该测试中您可以同时测试所有模块。再次,您想测试成功的情况和失败的情况。

在“基本示例”中,要对代码进行单元测试,请编写一个模拟数据库层的模拟类。但是,您的模拟类并没有真正进入数据库:您只是将其与预期的输入和固定的输出一起预加载。用伪代码:

function test_ValidUser() {
    // set up mocking and fixtures
    userid = 23;
    db = new MockDB();
    db.fixedResult = { firstName: "John", lastName: "Doe" };
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);
    expectedResult = "John Doe";

    // run the actual test
    actualResult = userController.displayUserAsString(userid);

    // check assertions
    assertEquals(expectedResult, actualResult);
    db.assertExpectedCall();
}

这是您如何测试正确报告的缺失字段的方法:

function test_IncompleteUser() {
    // set up mocking and fixtures
    userid = 57;
    db = new MockDB();
    db.fixedResult = { firstName: "John", lastName: "NA" };
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);

    // let's say the user controller is specified to leave "NA" fields 
    // blank
    expectedResult = "John";

    // run the actual test
    actualResult = userController.displayUserAsString(userid);

    // check assertions
    assertEquals(expectedResult, actualResult);
    db.assertExpectedCall();
}

现在事情变得有趣了。如果真正的数据库类行为不当怎么办?例如,出于不清楚的原因,它可能会引发异常。我们不知道它是否可以,但是我们希望我们自己的代码能够优雅地处理它。没问题,我们只需要使我们的MockDB抛出异常即可,例如,通过添加如下方法:

class MockDB {
    // ... snip
    function getUser(userid) {
        if (this.fixedException) {
            throw this.fixedException;
        }
        else {
            return this.fixedResult;
        }
    }
}

然后我们的测试用例如下所示:

function test_MisbehavingUser() {
    // set up mocking and fixtures
    userid = 57;
    db = new MockDB();
    db.fixedException = new SQLException("You have an error in your SQL syntax");
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);

    // run the actual test
    try {
        userController.displayUserAsString(userid);
    }
    catch (DatabaseException ex) {
        // This is good: our userController has caught the raw exception
        // from the database layer and wrapped it in a DatabaseException.
        return TEST_PASSED;
    }
    catch (Exception ex) {
        // This is not good: we have an exception, but it's the wrong kind.
        testLog.log("Found the wrong exception: " + ex);
        return TEST_FAILED;
    }
    // This is bad, too: either our mocking class didn't throw even when it
    // should have, or our userController swallowed the exception and
    // discarded it
    testLog.log("Expected an exception to be thrown, but nothing happened.");
    return TEST_FAILED;
}

这些是您的单元测试。对于集成测试,您不使用MockDB类。而是将两个实际的类链接在一起。您仍然需要固定装置。例如,您应该在运行测试之前将测试数据库初始化为已知状态。

现在,就职责范围而言:您的代码应期望代码库的其余部分按照规范进行实现,但是还应准备好在其余部分拧紧时妥善处理。你是不是负责测试其他代码不是你自己的,但你负责使你的代码弹性的另一端行为不端的代码,你也有责任测试代码的应变能力。这就是上面的第三项测试。


您是否阅读了问题下方的评论?OP编写了“测试”,但是他的意思是“验证检查”和/或“异常处理”
Doc Brown,

1
@tdammers:造成误解,我的意思是实际上是对异常进行处理。.无论如何,感谢完整的答案,最后一段是我想要的。
rdurand

1

我尝试通过以下三个主要原则进行编码:

  • 雅尼

所有这些都会使您冒着编写在其他地方重复的验证代码的风险。如果验证规则发生更改,则需要在多个位置进行更新。

当然,在将来的某个时候,您可能会重新平台化数据库(它确实发生了),在这种情况下,您可能会认为将代码存放在多个地方会更有利。但是...您正在为可能不会发生的事情编码。

任何其他代码(即使它永远不会更改)也是开销,因为将需要编写,读取,存储和测试它们。

以上所有都是正确的,您根本不做任何验证都是您的疏忽。要在应用程序中显示全名,您需要一些基本数据-即使您不验证数据本身。


1

用外行的话来说。

没有“数据库”“应用程序”之类的东西。

  1. 一个数据库可以被多个应用程序使用。
  2. 一个应用程序可以使用多个数据库。
  3. 数据库模型应强制执行数据完整性,包括在插入操作中未包含必填字段时引发错误,除非在表定义中定义了默认值。即使您绕过该应用程序直接将行插入数据库中,也必须这样做。让数据库系统为您完成它。
  4. 数据库应保护数据完整性并抛出错误
  5. 业务逻辑必须捕获这些错误,并向表示层抛出异常
  6. 表示层必须验证输入,处理异常或向用户显示悲伤的仓鼠。

再次:

  • 数据库->抛出错误
  • 业务逻辑->捕获错误并引发异常
  • 表示层->验证,抛出异常或显示可悲的消息。
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.