什么是垂直软件开发?


敏捷方法现在可能很普遍,并且有了它,增量方法的概念应该被开发社区所了解和利用。尽管如此,在与开发人员交谈时,我仍然发现它的理论与它在日常开发实践中的应用之间存在脱节。

我认为这种脱节部分是由于我们分层构建解决方案的方式,以及我们在创建用户故事和最终关联代码时可能不知不觉地倾向于使用相同的分层方法。

在本文中,我将简要介绍敏捷带来的好处,以及在 SSENSE 我们如何尝试利用分层架构(Hexagonal、Clean、Onion 等),同时仍然获得与增量方法相关的收益。

敏捷方法
2001 年发布了敏捷宣言,并发布了一系列旨在潜在更好的软件开发方法的原则。从它的原则来看,我想把重点放在欢迎变​​更和频繁交付软件上。

这两个原则通常被转化为将给定软件的开发分解为更小的部分,这些部分可以提供增强的功能并以更精细的方式持续交付。原因很简单,因为没有人确切知道期望的是什么,最好实现它的一小部分,看看我们从中学到了什么,提出更多问题,并在下一个版本中调整/增强其功能。

如果我们看一下最常见的敏捷框架之一 Scrum,我们会发现有类似的原则,例如迭代性质(检查和适应)以及与将基于给予优先权。

一个常见的迭代,一个冲刺,将由通常以用户故事的形式呈现的订购项目列表组成:“作为客户,我应该……”。.

当我们采用分层架构时,让我们看看这如何转化为开发过程。

具有分层架构的传统方法

图 1. 分层架构和每层中的通用组件。

在此示例中,我们使用领域驱动设计和无服务器 (AWS Lambda) 支持的 API,如图 1 所示,但您可以将它们替换为其他架构样式和集成模式中的那些,例如六角形或清洁架构。

开发人员采用的传统方法是关注每一层的元素,以指导用户故事的创建方式和实现方式。

这意味着专注于开发将持久保存和检索实体的存储库,或者尝试编写实体及其值对象的整个预期行为。

图 2 说明了一个常见的情况,在实现整个存储库功能之前,域没有取得任何进展。

这种横向方法有一些与之相关的负面影响:

  • 由于一个特性只有在它可以暴露给它的客户时才可用,如果 UI 层没有连接到其他层,那么交付给定组件所提供的价值就会降低。
  • 从上层开发另一个组件时获得的新知识可能需要您更改以前开发但未使用的组件。

现在让我们来看看垂直方法。

垂直纵向发展
在垂直开发方法中,您查看用户故事并在每个层和组件中实施实现目标所需的最少功能。

图 3. 垂直方法实现给定功能所需的每一层的部件。

在此示例中,您不再专注于一次完成每一层,而是开发实现成功所需的部分。然后,您开始以迭代方式扩展功能,方法是实施非主要场景、替换模拟或将代码从给定层重构到单独的层中。

图 4. 每次迭代都只执行最少的代码来提供给定的功能。

这种方法有以下好处:

  • 要求你只写你需要的代码
  • 使编写仅与开发的代码相关联的测试变得更容易
  • 与更小/更集中的代码审查自然对齐
  • 减少层间潜在的阻抗失配
  • 让你的站立更有效率

让我们看看每个人如何协同工作以取得更好的结果:

  • 要求你只写你需要的代码

传统上,您会尝试完全开发每一层。这意味着您的目标是编写代码来处理所有成功和失败路径、与特定实体关联的所有函数及其持久性。
这通常需要您尝试并预测未来的问题,或者仅基于一次性使用来创建抽象。
在垂直方法中,您会选择只开发成功路径,并有意推迟真正持久性介质的实施,选择模拟或简化版本。
从本质上讲,这意味着需要生成的代码更少,可以更快交付并涵盖整个用例的特定方面。
  • 使编写仅与开发的代码相关联的测试变得更容易

我们现在应该都知道测试驱动开发 (TDD) 的好处。打破先写代码再写测试的习惯很难遵循它,这不足为奇。
使用更小、更集中的方法,我发现这种困难会减少,因为您正在处理一个非常狭窄的子集,其中包含稍后将被拆分为单独的函数/类的代码。
  • 与更小/更集中的代码审查自然对齐


有一项已知的研究表明,正在审查的代码的大小与发现问题的能力之间存在关联。该研究表明,我们应该以拥有(或审查)小型 PR 为目标,但这不是传统方法往往会发生的情况。

图 5. 根据审查代码的大小发现的缺陷数量。来源SmartBear

为满足部分用例而编写的代码更少,您自然会交付更小的东西,并且由于其重点,审阅者更容易理解。
因为您正在更快地交付更有针对性的东西,审阅者可以评估并提供可以帮助您进行下一次迭代的反馈。不再工作 3 天只收到反馈,这些反馈基本上会使您所做的 80% 无效!

  • 减少层间潜在的阻抗失配

在传统方法中,在处理将接收请求并发回响应的 UI 层之前处理一个组件(例如持久性)的情况并不少见。

虽然完全有效,但随着您开发代码并开始提出更多问题,您对问题的理解以及解决方案也会随之发展。突然之间,您创建的模型不再适用或行为发生了变化。

这意味着您创建的代码片段甚至在使用之前就必须更改。根据变化,多层会产生涟漪效应。

通过使用垂直方法,因为您只编写了最少的代码并减少了开始时的层数,所以您需要根据新知识或同行评审反馈进行更改的移动部分更少。

  • 让你的站立更有效率

我不了解你,但我有过一些站会,其中似乎滥用了现在进行时“我正在研究这个…… ”。

似乎不清楚范围是什么,即使对于开发人员也是如此。它甚至可能比他们所能掌握的更大。因此,为“我今天在做什么”和任何相关的障碍提供更精确的定义可能很难定义。

在垂直方法中,由于关注范围更窄,范围更容易理解。例如,您并不是要实现整个客户存储库,而只是实现插入功能的快乐路径,以及其他层中的对应功能。

作为一名开发人员,我将能够更好地在站会期间提供反馈并在稍后寻求帮助。

在我们面临在我们自己的环境中实际应用这种方法的任务之前,这一切看起来都很简单。一种常见的反应是放弃并将您的故事标记为无法垂直切片的故事。这可能是因为我们长期以来一直以某种方式做事,以至于任何其他方式似乎都不适用。

为避免出现这种情况,让我们使用一个虚构的示例来逐步了解方法。

一步步演示
这个例子为这种方法提供了一个背景。它是设计出来的,因此我们可以以一种可管理的方式剖析这些步骤。

假设您必须提供允许客户在线下订单的功能。作为验收标准的一部分,您知道每个订单最多只能接受 10 件商品,并且您不应允许在处理同一客户的另一个订单时下订单。

第 1 步 — 确定主要和次要场景
情况并非总是如此,但通常我们首先对快乐之路感兴趣,因此这将是我们的主要场景。
所有其他情况,通常与某种形式的未能遵守这条幸福之路有关,都是次要的。
在我们的示例中,主要场景是订单格式正确(少于 10 件)并且没有来自同一客户的其他订单正在处理的场景。
次要的可能是:

  • 有超过10项
  • 同一客户的另一个订单正在处理中


第 2 步——用尽可能少的抽象开发主要场景
在我们的主要场景中,我们将接收下订单请求及其负载并将订单保存在数据库中。
如果您遵循分层架构,例如六边形架构,您最终可能会得到以下结构。

图 6. 完全实现给定功能后的潜在组件列表。

在此步骤中,您可以在 UI 层中完成所有操作,而不是尝试在一次迭代中完成所有操作。

如您所见,我什至选择以尽可能简单的方式保存它。另一种方法将完全跳过持久性,但在这个人为的例子中它似乎是合理的。


第 3 步 — 处理抽象
现在您已经有了基本的工作和更多的清晰度——您可能已经提出了一些问题,这些问题现在增加了您对该领域的了解——是时候重构/重写一些代码以将其推向正确的抽象。

这意味着,例如,实际定义命令/处理程序并创建您的领域模型,在这种形式下看起来更像是简单的数据结构。

请注意,虽然此处未显示,但我们预计会开始为其中一些抽象添加测试,并根据您的技术,利用模拟来替换持久层等依赖项。

一个不特定于垂直方法的关键方面是考虑尽可能推迟数据模型,这样您就可以更好地理解甚至尽可能晚地选择合适的技术;RDBMS、文档、图形、键值等。

在前面的代码示例中,您可以看到我们只是简单地添加域模型的 JSON 版本,而不用真正担心实际的访问模式和关系。


第 4 步 — 处理次要场景
因为您有一个工作示例和适当的最小抽象,所以您可以继续前进。此时,您的实际用例和上下文将决定。但出于我们示例的目的,我会首先添加项目验证,因为它似乎是两者中更常见的场景。

因为我们采用领域驱动方法,所以这种验证将在领域级别进行。这意味着我们现在将开始以值对象的形式将适当的域逻辑直接添加到Order实体或其属性。

随着领域模型的发展,您将有机会重新访问持久性并更新相应的基础设施实现。

测试将效仿,并得到增强以记录其新场景和组件。

此步骤可以产生其他步骤,每个备选场景或什至是一个额外的方面。例如,您可能建议在 UI 级别添加有效负载的基本验证,利用一些 OpenAPI 规范验证。

实际上,上述每个步骤都可以分解为一个或多个拉取请求。根据复杂性,尤其是在第 2 步之后,您甚至可以让多个开发人员处理同一步骤的不同方面。

结论
虽然没有“放之四海而皆准”的软件开发方法,但我相信本文介绍的垂直方法适合大多数情况。它很好地将敏捷原则与代码开发实践结合在一起。
采用它并不是一项复杂的工作,但需要耐心和纪律,因为它会迫使您考虑如何以不同的方式组织您的工作。