阅读 574

[译] 真相就在代码中

真相就在代码中

购物应用模型,requirementsascode 的示例代码

早晚有一天,每个程序员都会听到这样一句话:

“真相只能在一处找到:代码中。”

—— Robert C. Martin,代码整洁之道

但这句话是什么意思呢?

敏捷宣言中指出“可工作的软件胜过繁琐的文档”。

即便开发人员一直都在撰写繁琐的文档以描述软件的行为。代码也在做着相同的事。

代码注释、外部规范也在记录软件的行为,但是当代码被修改时它们可能不会被同步更新。然后它们很快就不再能表达代码的行为了。

相反,代码始终都能表达软件的行为。因为正是它定义了这些行为。

这就是为什么说真相存在于代码中。

为阅读你代码的人考虑

代码是一种文档。任何文档都应该能够被它的读者理解。

代码的读者可能是一个编译器,解释器或是其他开发人员。

所以你的代码仅能编译通过是不够的。你还要保证其他开发人员能够读懂它。未来他们需要在你代码的基础上工作,修改它,扩展它。

一个关于使代码容易阅读的常见建议是编写整洁的代码。整洁的代码指的是使用易懂的语言命名变量和方法的代码。这也使得很多代码注释变得不必要了。

整洁的代码应该能表达意图:使用者通过调用该方法能做什么。而不是怎么做

猜测一下这个方法是做什么的:

    BigDecimal addUp(List<BigDecimal> ns){..}复制代码

如果是这么写呢:

    BigDecimal calculateTotal(List<BigDecimal> individualPrice){..}复制代码

整洁的代码是一个好主意。但我认为这还不够。

知识共享的重要性

当有一个新需求时,你需要评估实现该需求对现有代码的影响。

如果你的软件已经存在了一段时间,这可能会是个挑战。我经常听到这样的对话:

X:我们不能继续开发 foo 特性了。

Y:为什么?

X:因为 Z 是唯一了解这块代码的人。我们要改动的代码就是他开发的。

Y:好吧,为什么不去问问他呢?

X:因为他病了/休假了/在开会/离职了。

Y:额……

事情就是这样。要想知道你的代码是否容易被理解,至少要有个人去尝试阅读它。

有这方面的技术。结对编程就是一个不错的选择。或者是和其他开发人员坐下来,一起过一遍你写的代码。

然而,如果参与一个项目的开发人员太多呢?如果一个研发团队的成员变化了呢?这使得编写容易被其他人理解的代码变得更困难了。

故事

整洁代码带给你正确的用语

问题是:你在代码中使用它们讲述怎样的故事呢?

我不知道。

但是对于一个典型的业务应用,我很清楚我希望在代码中读到怎样的故事。

在介绍一个简单的例子之后,我会简要描述那个故事。

手套商店的例子

作为一个软件的用户,我想要达到期望的目的。比如,我想拥有一双新手套在冬天可以给我的手保暖。

因此我上网找到了一家新开的线上手套专卖店。该店铺的网站可以让我购买手套。“基本流程”(也被称为“正常用例”)大概是这样的:

  • 系统以一个空购物车开始。
  • 系统展示一个手套的列表。
  • 我添加喜欢的手套到购物车。系统将这些手套加入到我的订单中。
  • 我选择结账。
  • 我输入配送信息和支付详情。系统保存这些信息。
  • 系统展示一份订单的详细信息。
  • 我确认信息。系统开始配送我的订单。

几天以后,我收到手套。

下面是我想在代码中读到的故事

第一章: 用例

故事的第一章是关于用例的。当我阅读代码时,我希望在代码中按照某个用例一步一步的达到期望的结果。

从一个用户的角度来看,我想弄明白当出现错误时系统是如何应对的。

我还想搞清楚流程中可能的分支。例如,用户企图从支付详情页回到配送信息页时会发生什么?用户可以这样操作吗?

我想知道每一个用例中的不同部分对应哪块代码。

那么一个用例由哪些零件构成呢?

用例的基础零件是使用户离期望结果更近一步的步骤。比如:“系统展示一个手套的列表。”

不是所有用户都可以执行某一步骤,只有特定用户组的成员(“行为者”)才可以这么做。例如,终端消费者买手套。销售人员向系统中添加新款手套的报价。

某些步骤由系统主动执行。例如在展示手套列表时。无需用户交互。

而有的步骤则是用户交互的结果。系统响应某些用户事件。例如:用户输入配送信息。系统保存这些信息。

我想知道这些事件中包含哪些数据。配送信息包含用户的姓名,地址等等。

用户在任何给定的时间点上只能执行一部分步骤。用户只有在输入配送信息后才可以填写支付详情。因此每一个用例中都有一个定义了该用例中所有步骤执行顺序的流程。以及一个根据系统当前状态,表示系统是否可以响应用户的操作的条件

要理解代码,你需要一个简单方法来了解几件事情。

对于一个用例(例如“买手套”):

  • 步骤流程

对于每个步骤:

  • 哪些行为者有执行的权限(也就是哪个用户组)
  • 在哪些条件下,系统可以响应
  • 该步骤是自发的还是基于用户交互
  • 系统响应

对于每个基于用户交互的步骤:

  • 用户事件(例如“用户输入配送信息”)
  • 伴随事件而来的数据

一旦我知道在哪里可以找到用例以及它的零件之后,我就可以深入研究了。

第二章: 通过组件分解步骤

让我们把你软件中一个封装的,可替换的组成单位称为一个组件。一个组件的职责可以被该组件之外的世界访问。

一个组件可能是:

  • 一个技术组件,比如数据库
  • 一个服务,比如“购物车服务”
  • 你的领域模型中的一个实体

这取决于你的软件设计。但不论你的组件是什么:你通常都需要若干个组件配合来实现用例中的某个步骤。

让我们来看看“系统展示一个手套的列表”这一步骤中的系统响应。你很可能需要开发至少两项职责。一个用来在数据库中查找手套,另一个用来把这些数据转变为一个页面。

当阅读代码时,我希望能了解以下这些内容:

  • 一个组件的职责是什么。例如:对数据库来说是“查找手套”。
  • 每个职责的输入/输出是什么。输入的例子:查找手套的规则。输出的例子:手套列表。
  • 谁来协调这些职责。例如:首先查找手套,然后将结果转换成一个网页。

第三章:组件做什么

组件的代码用来实现它的职责。

这通常出现在领域模型中。领域模型使用和业务领域相关的术语。

举例来说,手套可以是一个术语。订单也可以是一个术语。

领域模型用于描述每个术语的数据。每个手套都有颜色,品牌,尺码,价格等数据。

领域模型还用来描述基于这些数据的运算。一个订单的总价是该订单中用户购买的所有手套价格的总和。

一个组件还可以是类似数据库这样的技术组件。该组件的代码就要解决如何在数据库中创建、查找、更新、删除数据的问题。

讲述你的故事

你的故事可能看起来和上面提到的故事很相似,也可能完全不同。不论你的故事是怎样的,编程语言都给了你极大的自由来讲述你的故事。

这是件好事情,因为它允许开发人员适应不同的情景与需求。

这也承担了由开发人员讲述太多不同故事(哪怕是针对同一个产品)带来的风险,使得理解其他人编写的代码变得更困难了。

解决这个问题的一个办法是使用设计模式。它们可以帮你合理的组织代码。你可以在团队中甚至是团队间就这种通用结构达成一致。

例如:Rails 框架就是基于众所周知的模型、视图、控制器模式的。

模型用于放置领域数据

视图是客户端用户界面,比如 HTML 页面。这是用户事件的来源。

控制器在服务器端接收用户事件。它负责流程

因此,如果多个开发人员使用 Rails,他们就知道在哪里能找到和故事中特定部分相关的代码。

他们可以在分享他们的见解时找出缺失的东西。然后,他们就可以进一步的就约定在哪里放置故事的模块达成一致。

如果这些适用于你,那就好了。但我想比这更进一步。

代码即需求

很多客户问我如何处理长期的软件文档。

在敏捷开发中,如何创建软件维护文档?

到目前为止都实现了哪些需求?

在哪里能找到它们在代码中的实现?

很久以来我都没有满意的答案。当然,除了良好编写的自动化测试,整洁的线上代码,以及共同认知的重要性之外。

但是在几年前,我开始思考:

如果真相在代码中,那么代码也应该能讲述真相。

换句话说:如果你非常小心的在代码中讲述你的故事,为何还要再说一遍呢?

需要有更好的方法。它可以提取故事,并基于它生成文档。非技术相关方也能理解的文档。

始终保持最新状态的文档,因为正是它的来源定义了软件行为。

唯一可靠的来源:代码自身。

在许多次尝试之后,我有了一些成果。我把它们发布在一个名为 requirementsascode 的 GitHub 项目中。

它是如何工作的

  • UseCaseModel 实例用于定义行为者用例,它们的流程以及步骤。它讲述故事的第一章。在本文的开头你能找到一个这种模型的例子。
  • 用例模型配置 UseCaseModelRunner 实例。每个用户都有自己的运行程序,因为每个用户选择执行的用例路径可能不同。
  • 运行程序通过调用后端的系统响应来响应前端的用户事件。前端只能通过运行程序和后端通信。
  • 但是运行程序只有当用户在流程中的正确位置且满足步骤的条件时才会响应用户。例如:运行程序只有在用户已经输入了配送信息之后才会响应“进入支付详情页”事件。
  • system reaction 是一个单例方法。方法内部负责协调不同组件以实现该步骤,就像在第二章中描述的那样。
  • 第三章已经超出了 requirementsascode 的范畴。它留给应用程序决定。这使得 requirementsascode 可以兼容任意的软件设计。

因此,基于 UseCaseModel,UseCaseModelRunner 控制了对用户可见的软件行为。

通过 requirementsascodeextract, 你可以从同一个配置运行程序的用例模型中生成文档。这样,文档就可以始终表达软件的行为了。

Requirementsascodeextract 使用了 FreeMarker 模板引擎。它允许你生成任何你喜欢的纯文本文档。例如 HTML 页面。进一步的处理可以生成其它格式的文档,例如 PDF。

你的反馈可以帮我改进这个项目

我从几年前就开始了 requirementsascode 这个项目,直到最近才将它公开。与最初相比,它已经得到了极大的改善。

为了了解这种方法是否具有可扩展性,我尝试在一个有数千行代码的项目中使用。被证明是有效的,我也在一些更加小型的应用中尝试过。

到目前为止,requirementsascode 一直都是我的业余项目。

这就是为什么我需要你们的帮助。请给我一些反馈。

你觉得这个想法怎么样?你能想象它在你的软件上下文中有效吗?还有其他的反馈意见吗?

你可以在评论区给我留言或是在 TwitterLinkedIn 和我联系。

你可以 clone 这个项目并亲自尝试一下。

也可以帮助在代码中记录真相。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划