软件的复杂性: 必要的、意外的和偶然的
随着我们工程领域的发展,软件的复杂性似乎变得越来越难以控制了。为了正确理解如何处理不断增加的复杂性,重要的是要分辨出软件复杂性的三种基本类型:基本的、偶然的和附带的复杂性。
基本的复杂性
这种类型的软件复杂性与我们试图建模的领域的复杂性有关。更具体地说,我们正在处理管理业务流程的业务政策规则。当试图实现和自动处理这些政策规则时,我们会遇到不同程度的复杂性。
无论我们如何努力,这种复杂性都是无法被削减的。例如,如果管理索赔处理的业务政策规则需要15个不连续的步骤,我们不能通过跳过一些步骤来简化规则,将处理过程压缩到只有几个基本步骤。
总之,基本的复杂性是不可避免的,这也是我们作为软件工程师受雇于人的真正原因。
意外的复杂性
虽然基本的复杂性是值得拥有的(为客户提供价值,为企业带来竞争优势,并为工程师提供令人兴奋的职业机会),但意外的复杂性会产生反作用。这种类型的复杂性是由于缺乏适当的理解、教育和培训造成的,也可能是由于业务和软件工程之间沟通不畅造成的。
意外的复杂性表现为不良的架构、不良的设计、不良的代码以及不良的软件工程流程。因为意外的复杂性是由上述一些或所有的因素造成的,所以摆脱它的唯一方法是消除其原因。
消除意外复杂性的原因的最快捷的方法是教育、培训、指导和辅导。总有改进的余地,我们越是把日常活动的重点放在学习上,我们就能越快地把偶然的复杂性降到最低。
偶然的复杂性
这种类型的复杂性是最难处理的一种。而且它绝对是最具有反作用的复杂类型。
当我在大学时,我了解到编写软件包括接收数据,以编程方式操作它,将其存储在本地,然后将其发送到某个地方。在我接受教育的过程中,我从未想过要学习负载均衡器、各种数据存储(SQL、NoSQL、memcached)、容器(Vagrant、Docker Kubernetes)以及其他工具和技术,每一种都只是提供应用程序的一个部分。那时,这些基础设施工具被视为仅仅是恼人的细节。
使这些恼人的细节变得如此突出的根本原因在于Unix哲学: "只做一件事并把它做好"。工具遵循渐进式的进化路径;每个工具开始时都是一个简单的实用程序,旨在挠痒痒。通常,"挠痒痒 "包括解决一些已知的问题。从那时起,这些工具通过改进以前的能力而不断发展。当然,这种改进从来都不是着眼于消除复杂性。
例如,Git是一个比Subversion更好的版本控制工具(Git是Subversion的后续发展),但Subversion比Git简单得多。
如何降低复杂度
降低复杂度的唯一方法是去除事物。与其继续在进化的道路上为现有的东西建立更好的版本,我们必须消除一些东西。
例如(一个非常简化的例子),我们的工具箱里目前可能有两个工具。我们把这两个工具捆绑在一起,形成我们的工作流程,这种安排有助于我们完成工作。
如果我们现在摆脱这两个工具,建立一个工具,做以前每个工具都知道如何做的两件事,我们就消除了附带的复杂性。一个单一的工具不仅消除了界面,还减少了之前我们需要两个工具来处理的两个问题的表面积。
想象一下,如果我们把几十个工具集中到一个单一的工具中,我们会得到怎样的简单性收益。通过这样做,我们消除了一大堆附带的复杂性,因为我们不需要担心工具之间的接口和重叠问题。
目前,在尽可能地消除附带的复杂性方面,最好的技术是.Net。它目前的迭代(C#-Visual Studio-Azure)提出要消除大量的专业工具,这些工具带来了繁重的开销。让我们来看看我们正在努力驯服的附带复杂性的一些方面。
代码即文本的复杂性
我们把代码写成文本,这就带来了引入语法错误的高风险。当我们把一些代码写成文本时,我们要求编译器来阅读它,而很多时候它无法阅读代码(阅读代码的尝试导致了编译器错误)。好的编辑
好的编辑器(例如,Visual Studio和Visual Studio Code)建议使用编程语言,而不仅仅是机械地协助格式化文本。
好的工具(编辑器)通过将各种较小的工具合并到一个大的工具中,使附带的复杂性最小化。Visual Studio理解编程语言,并提供智能提示/自动完成功能。访问控制和协作也被植入这个工具。除此之外,重构也是内建的,还有版本控制、特征标志、函数和类型版本控制。
这样先进的编辑器在减少甚至彻底消除附带的复杂性方面有很大的作用。强烈建议我们充分利用这种先进的工具。
基础设施的复杂性
与运行我们代码的机器有关的一切都被称为计算基础设施。队列、防火墙、网络、负载均衡器、扩展、安全、监控、数据库、分片等等。作为专注于不间断地提供价值的软件工程师,我们只对与数据、业务策略规则处理和客户的工作感兴趣。上述所有的基础设施概念仅仅是烦人的细节,并没有给客户带来任何价值。因此,我们将基础设施视为附带的复杂性(必要的邪恶)。我们的付费客户不可能不关心我们的排队、扩展、监控等政策。
诸如Azure这样的最佳技术在抽象出上述许多基础设施问题方面有很大的作用,使我们从许多附带的复杂性中解脱出来。
部署的复杂性
完成的代码(即候选版本)需要从一台机器同步到另一台机器。从概念上讲,这种操作应该是微不足道的。但在实践中,如何快速、安全地完成这种同步是一个挑战。为什么会这样呢?让我们来数数有哪些方法:
- 打包代码(Docker容器、tarball、webpack、jars、git checkout...)
- 测试代码(代码覆盖率、变异测试、浏览器测试/Selenium)。
- 同步代码(git推送、Docker注册、工件托管、S3、CDN)。
- 启用新代码(Kubernetes、反向代理、Capistrano、交换符号链接)
- 推广代码(功能标志、暗中启动、隔离启动、蓝绿部署、数据库迁移、API版本)。
同样,Azure在尽量减少部署代码时附带的复杂性方面取得了一些进展。
API的复杂性
理想情况下,使用API不应该比调用一个函数更难。然而,情况几乎不是这样的。认证、速率限制、重试、错误等都会使这些调用变得附带复杂。
我们正在试图处理这些挑战的一些方法: SOAP / REST / HTTP+JSON / gRPC / GraphQL / Protobuf。
这类附带的复杂性还有待解决。有一些希望,随着Azure等平台的成熟,它们将提供工具,以减少我们在使用API时面临的选择丛林。
总结
复杂性是运送高质量软件的最大敌人。一种类型的复杂性(即基本复杂性)是可取的,因为它为企业和客户提供了竞争优势。其他类型的复杂性(意外的和偶然的)是不可取的,因为它们对付费客户完全没有增加任何价值。
通过明智地选择我们的技术栈,我们可以避免一些偶然的复杂性。而通过选择我们的培训/教育路径,我们也可以避免几乎所有的意外复杂性。
总而言之,那些设法尽量减少/消除他们运营中的大量意外和偶然的复杂性的企业将保持竞争力并在市场上取得胜利。