复杂性的本质与应对策略
1 引言
编写计算机软件是人类历史上最纯粹的创造性活动之一。程序员不受物理定律等实际限制的束缚,可以创造出激动人心的虚拟世界。编程不需要像芭蕾舞或篮球那样的高超身体技能或协调能力。编程所需的一切就是一个创造性的头脑和组织思路的能力。如果你能想象一个系统,你可能就能在计算机程序中实现它。
这意味着编写软件的最大限制是我们的理解能力,即我们正在创建的系统。随着程序的演变和获得更多功能,它变得复杂,其组件之间存在微妙的依赖关系。随着时间的推移,复杂性逐渐积累,程序员在修改系统时越来越难以将所有相关因素记在心里。这会减慢开发速度并导致错误,这些错误会进一步减慢开发并增加其成本。任何程序的生命周期中,复杂性不可避免地增加。程序越大,参与开发的人越多,管理复杂性就越困难。
2 复杂性的定义
复杂性是指与软件系统的结构相关的任何事物,这些事物使得理解并修改系统变得困难。复杂性可以有多种形式。例如,理解一段代码的工作原理可能很困难;实现一个小的改进可能需要大量的努力;或者不清楚需要修改系统的哪些部分以进行改进;在修复一个错误时可能会遇到困难,而不会引入另一个错误。如果一个软件系统难以理解和修改,那么它是复杂的;如果它易于理解和修改,那么它是简单的。
你也可以从成本和收益的角度来考虑复杂性。在一个复杂系统中,即使实施小小的改进也需要大量的工作。在一个简单系统中,可以更轻松地实施更大的改进。
2.1 复杂性的表现形式
复杂性以三种一般方式表现出来,每种表现形式都使得开展开发任务变得更加困难。
变更放大
复杂性的第一个症状是,一个看似简单的变更需要在许多不同的地方修改代码。例如,考虑一个包含多个页面的网站,每个页面都显示一个带有背景颜色的横幅。在许多早期的网站中,颜色在每个页面上都明确指定。为了改变这样的网站的背景,开发者可能不得不手动修改每一个现有的页面;对于拥有成千上万页面的大型网站来说,这几乎是不可能的。幸运的是,现代网站采用了一种方法,在这种方法中,横幅颜色在中心位置指定一次,所有单独的页面都引用这个共享值。使用这种方法,整个网站的横幅颜色可以通过单次修改来改变。良好设计的目标之一是减少受每个设计决策影响的代码量,这样设计变更就不需要进行大量的代码修改。
认知负荷
复杂性的第二个症状是认知负荷,它指的是开发者完成任务所需了解的信息量。较高的认知负荷意味着开发者需要花费更多时间学习所需的信息,并且由于遗漏了重要信息,出现错误的风险更大。例如,假设C语言中的一个函数分配内存,返回指向该内存的指针,并假设调用者将释放内存。这增加了使用该函数的开发者的认知负荷;如果开发者未能释放内存,就会发生内存泄漏。如果系统可以重构,使得调用者无需担心释放内存(分配内存的同一模块也负责释放它),这将减少认知负荷。认知负荷以多种方式产生,例如具有许多方法的API、全局变量、不一致性以及模块之间的依赖关系。
未知的未知
复杂性的第三个症状是不明显哪些代码片段必须被修改以完成任务,或者开发者必须拥有什么信息才能成功执行任务。图2.1(c)说明了这个问题。网站使用一个中心变量来确定横幅背景颜色,所以看起来很容易改变。然而,一些网页使用更深的背景颜色来强调,而这个更深的颜色在各个页面中明确指定。如果背景颜色发生变化,那么强调颜色也必须改变以匹配。不幸的是,开发者不太可能意识到这一点,所以他们可能会改变中心的bannerBg变量而不更新强调颜色。
2.2 复杂性的后果
随着复杂性的增加,它会导致变更放大、高认知负荷和未知的未知数。因此,实现每个新功能需要更多的代码修改。此外,开发人员需要花费更多时间获取足够的信息以安全地进行更改,在最坏的情况下,他们甚至找不到他们所需的所有信息。底线是,复杂性使得修改现有代码库变得困难和风险重重。
3 复杂性的原因
复杂性是由两件事情引起的:依赖性和模糊性。依赖性是指给定的一段代码不能被孤立地理解和修改;这段代码以某种方式与其他代码相关联,如果要理解和/或修改这段代码,必须考虑和/或修改其他代码。模糊性通常与依赖项有关,在那里不明显存在依赖关系。例如,如果系统中添加了一个新的错误状态,可能需要向存储每种状态的字符串消息的表中添加一个条目,但是查看状态声明的程序员可能不会明显地看出消息表的存在。不一致性也是模糊性的一个主要贡献因素:如果同一个变量名用于两个不同的目的,开发者不会明显地知道特定变量服务于哪个目的。
| 依赖性 | 模糊性 |
|---|---|
| 方法签名创建依赖性 | 添加新状态需要更新多个位置 |
| 网络协议发送端和接收端的依赖 | 不一致性导致的模糊性 |
3.1 依赖性
依赖性是软件系统中不可避免的一部分,但我们可以通过设计来减少它们的数量和复杂性。例如,在图2.1(a)中,每个页面的背景颜色独立指定,导致所有页面相互依赖。而在图2.1(b)中,通过集中管理背景颜色,消除了这些依赖关系。
3.2 模糊性
模糊性通常与依赖项有关,在那里不明显存在依赖关系。例如,如果系统中添加了一个新的错误状态,可能需要向存储每种状态的字符串消息的表中添加一个条目,但是查看状态声明的程序员可能不会明显地看出消息表的存在。不一致性也是模糊性的一个主要贡献因素:如果同一个变量名用于两个不同的目的,开发者不会明显地知道特定变量服务于哪个目的。
graph TD;
A[复杂性] --> B(依赖性);
A --> C(模糊性);
B --> D[方法签名];
B --> E[网络协议];
C --> F[添加新状态];
C --> G[不一致性];
4 复杂性的递增性质
复杂性并非由单一的灾难性错误引起;它是在许多小块中逐渐积累的。单个依赖性或不明确性本身不太可能显著影响软件系统的可维护性。复杂性之所以产生,是因为成百上千的小依赖性和不明确性随时间逐渐累积。最终,这些小问题的数量如此之多,以至于系统中每一个可能的改变都会受到其中几个问题的影响。
复杂性的递增性质使得它难以控制。你会很容易地说服自己,你当前的改变引入的一点点复杂性不算什么大事。然而,如果每个开发人员都对每次改变采取这种态度,复杂性就会迅速累积。一旦复杂性已经累积,就很难消除,因为修复单一依赖项或模糊性本身并不会产生很大的影响。为了减缓复杂性的增长,你必须采纳一种“零容忍”的哲学。
4 复杂性的递增性质(续)
复杂性的递增性质使得它难以控制。你会很容易地说服自己,你当前的改变引入的一点点复杂性不算什么大事。然而,如果每个开发人员都对每次改变采取这种态度,复杂性就会迅速累积。一旦复杂性已经累积,就很难消除,因为修复单一依赖项或模糊性本身并不会产生很大的影响。为了减缓复杂性的增长,你必须采纳一种“零容忍”的哲学。
4.1 控制复杂性的策略
为了有效地控制复杂性,开发团队需要采取一系列策略。以下是一些常见的方法:
- 模块化设计 :将系统划分为独立的模块,每个模块负责特定的功能。模块之间的接口应尽量简单明了,减少依赖性。
- 持续重构 :定期审查和改进代码,消除不必要的复杂性。重构不仅可以提高代码质量,还能增强系统的可维护性。
- 代码审查 :通过代码审查,确保每位开发人员都遵循最佳实践,及时发现并修正潜在的复杂性问题。
- 自动化测试 :编写自动化测试用例,确保每次修改不会引入新的问题。测试覆盖率越高,系统的稳定性越好。
| 策略 | 描述 |
|---|---|
| 模块化设计 | 将系统划分为独立的模块,每个模块负责特定的功能 |
| 持续重构 | 定期审查和改进代码,消除不必要的复杂性 |
| 代码审查 | 通过代码审查,确保每位开发人员都遵循最佳实践 |
| 自动化测试 | 编写自动化测试用例,确保每次修改不会引入新的问题 |
4.2 实际案例分析
让我们通过一个实际案例来更好地理解如何应对复杂性。假设我们正在开发一个电子商务平台,该平台包括用户管理、订单处理和支付系统等多个模块。随着业务的发展,系统逐渐变得复杂,导致开发和维护成本增加。为了解决这个问题,我们可以采取以下措施:
- 模块化设计 :将用户管理、订单处理和支付系统分别划分为独立的模块,每个模块有自己的接口和服务。
- 持续重构 :定期审查代码,优化性能瓶颈和冗余逻辑,确保代码简洁易懂。
- 代码审查 :建立代码审查机制,确保每位开发人员的代码符合规范,及时发现并修正潜在问题。
- 自动化测试 :编写单元测试、集成测试和端到端测试,确保系统的稳定性和可靠性。
graph TD;
A[电子商务平台] --> B(用户管理);
A --> C(订单处理);
A --> D(支付系统);
B --> E[模块化设计];
C --> F[持续重构];
D --> G[代码审查];
A --> H(自动化测试);
5 复杂性的识别与应对
识别复杂性是一项关键的设计技能。它允许你在投入大量努力之前识别问题,并允许你在替代方案中做出好的选择。判断一个设计是否简单比创造一个简单设计更容易,但一旦你能识别一个系统过于复杂,你可以使用这种能力来引导你的设计哲学朝向简单性。如果一个设计看起来很复杂,尝试不同的方法,看看是否更简单。随着时间的推移,你会发现某些技巧往往会产生更简单的设计,而其他技巧则与复杂性相关。这将使你能够更快地产生更简单的设计。
5.1 复杂性的识别
复杂性可以通过以下几个方面来识别:
- 代码的可读性 :难以理解的代码通常是复杂性的标志。如果一段代码需要花费很长时间才能读懂,说明它可能过于复杂。
- 修改的难度 :如果一个小的改动需要修改多个地方,说明系统存在复杂的依赖关系。
- 调试的难度 :如果调试一个错误需要很长时间,说明系统可能存在复杂的逻辑或依赖关系。
5.2 复杂性的应对
应对复杂性需要从多个角度入手,包括设计、开发和维护。以下是一些建议:
- 简化设计 :尽量减少不必要的功能和依赖关系,保持系统的核心功能简单明了。
- 优化代码结构 :通过合理的代码结构和命名约定,提高代码的可读性和可维护性。
- 使用设计模式 :适当使用设计模式可以有效减少复杂性,提高代码的复用性和扩展性。
- 持续改进 :复杂性是一个长期积累的问题,需要持续关注和改进。定期进行代码审查和重构,确保系统的健康和稳定。
5.3 复杂性的预防
预防复杂性的关键是提前规划和设计。以下是一些预防复杂性的建议:
- 明确需求 :在项目开始前,确保需求明确且合理,避免频繁的需求变更。
- 合理分工 :根据团队成员的专长,合理分配任务,确保每个人都能高效工作。
- 文档化 :编写详细的文档,记录系统的架构、设计和实现细节,方便后续的维护和扩展。
- 培训和沟通 :加强团队内部的培训和沟通,确保每位成员都了解系统的整体架构和设计原则。
通过以上措施,我们可以有效地识别和应对复杂性,确保系统的稳定性和可维护性。复杂性是软件开发中不可避免的问题,但我们可以通过科学的方法和合理的策略,将其控制在可接受的范围内,从而提高开发效率和产品质量。
8万+

被折叠的 条评论
为什么被折叠?



