基于快速失败的软件开发 - levelup


本文介绍了fail-fast 原理、它的优点、如何应用它以及我的个人经验。尽管看起来违反直觉,但快速失败会使您的应用程序更加健壮。使用快速失败原则,错误和故障会更快出现,这使得它们更容易修复。
如果本文启发您在代码库中应用快速失败原则,您可以立即开始使用它。即使您将该原则应用于单个文件,其积极效果也已经很明显。
您的自动驾驶汽车会在启动过程中检测到出现故障的传感器。你希望接下来发生什么?

  1. 汽车在传感器损坏的情况下行驶,让我们看看它能走多远。
  2. 汽车拒绝移动并通知您传感器损坏。

我猜你选择了选项二。我们可以将此选项称为快速失败选项。没有什么可怕的事情发生,因为汽车会尽快停止正常运行。在软件开发过程中可以应用类似的概念,这就是本文的全部内容。
 
听起来是个可怕的主意
软件开发人员使用许多技术来防止他们的程序失败。一些例子包括:

  • 回退
  • 稍后重试
  • 优雅降级

使用这些技术的程序会在发生错误后继续工作,但以后可能会以意想不到的方式失败。它们会导致缓慢或无声的故障。
默默地失败就像把头埋在沙子里。它没有解决根本问题,只是假装问题不存在。—弗拉基米尔·霍里科夫

不要把头埋在沙子里,而是要快速失败!快速失败的软件会尽早失败并尽可能地可见²。这听起来像是一个可怕的想法,但事实并非如此。
 
故障缓慢的自动驾驶汽车
让我们假设介绍中的自动驾驶汽车在传感器损坏的情况下尝试自动驾驶。它可以毫无问题地驶离停车场,但一旦我们到达开阔的道路,灾难就会袭来。损坏的摄像头导致汽车坠毁。车子全毁了,我们很幸运没有人受伤。
鉴于汽车的当前状态,确定碰撞原因是一个费力的过程。专家们只有在经过漫长而昂贵的调查后才发现损坏的传感器。
快速失败的汽车不会让这样的事故发生。传感器不会自行修复,但故障排除很容易,因为汽车会向您显示问题所在。
也许这是一个牵强附会的例子来说明问题。然而,类似的概念也适用于故障缓慢软件。慢故障软件也可能以意想不到的方式崩溃,因为小问题没有被注意到,使调试成为一个复杂而昂贵的过程。
 
快速失败的优势
故障快速软件中的错误和故障会更早地出现,使它们更容易重现和修复,这使得软件开发更加愉快。

  • 节约成本

进入生产环境的错误和故障更少,因为它们会更快出现¹。据 IBM 称,及早发现错误可以节省大量成本,因为一旦错误进入生产环境,修复错误的成本可能会高出五倍⁴。
  • 防止数据损坏

快速失败原则可防止软件进入无效状态,因此无法在数据库中持久化无效状态⁵。这是一个巨大的优势,因为数据损坏可能难以修复。
  • 没有银弹

你不能通过快速失败来解决所有问题,错误和失败仍然会发生。快速失败原则仅有助于尽快发现问题,从长远来看使您的应用程序更加健壮。
尽管快速失败有很多优点,但使用优雅降级等慢速失败原则也有其时间和地点。
 
实践中的原则
我们已经看到快速失败并不是一个可怕的想法,那么我们如何应用它呢?
尽管快速失败原则与语言无关,但我是一名 Java 开发人员,因此本节使用 Java 代码示例。
  • 断言

在出现问题之前,我们不应该失败。为了确定是否出现问题,我们使用断言。
断言是快速故障应用程序的基本构建块。断言是验证条件的简单代码段,如果验证失败,它们会抛出异常。¹。
大多数编程语言都有可用的断言库。如果没有,编写自己的断言方法很容易。我们可以在下面看到一个简单的例子。
public final class Assert {

    private Assert() {
        // Hide constructor to avoid instantiation
    }

    public static void notEmpty(final String obj, final String errorMessage) {
        if (obj == null || obj.isEmpty()) {
            throw new IllegalStateException(errorMessage);
        }
    }
}

好的断言库允许您传递错误消息,该消息将作为异常的一部分显示。此消息起着至关重要的作用,因为它允许您传达错误的内容,从而使问题易于解决。

  • 战术断言放置

为每个单独的变量赋值用断言向你的代码发送垃圾邮件是没有意义的。相反,问问自己断言如何有助于使常见问题易于发现和修复:
放置你的断言,以便软件更早地失败——接近原始问题——使问题易于发现。—吉姆·肖尔
我发现快速失败断言最有用的地方是创建不可变对象。顾名思义,不可变对象在我们实例化它们后永远不会改变。通过确保我们使用有效值实例化一个不可变对象,我们在以后使用它时不必担心它包含无效数据。
public class Person {
    private final String firstName;
    private final String lastName;

    public Person(String firstName, String lastName) {
        Assert.notEmpty(firstName, "firstName cannot be empty");
        Assert.notEmpty(lastName,
"lastName cannot be empty");

        this.firstName = firstName;
        this.lastName = lastName;
    }

   
// Getters, equals(), and hashCode() omitted for brevity
}

另一个使用快速失败断言的教科书示例是在启动参数的初始化期间。如果缺少启动参数或包含无效数据,您可以使用断言来确保应用程序无法启动。

  • 无需自毁

 听起来您的应用程序应该在遇到每个错误时自毁,但这并不是快速失败的重点。
假设您的应用程序公开了一个 REST 端点。我们不应在收到无效请求时关闭应用程序。相反,我们应该忽略传入的请求并向客户端发送错误消息。我们仍然可以将这种方法视为快速失败,因为我们会尽快失败并传达简单的错误消息。