什么是复杂性?
最近读完了《软件设计哲学》,第二章探讨了软件复杂性的话题。
《软件设计哲学》一书实际定义了复杂性:
“复杂性是指与软件系统的结构相关的任何使其难以理解和修改的事物。”
换句话说,复杂性可以有多种形式,并且不一定与性能有任何关系,你的代码可以是高性能的但仍然很复杂
我想在本文中分享本书中的一些关键定义和见解。但首先,让我们想象一个您可能已经经历过的常见情况……
一个简短的恐怖故事
让我们深入探讨一个你们中的许多人可能经历过或将要经历的恐怖故事。
-
它从一个简单的 CRUD 任务管理应用程序开始。代码干净、模块化且易于维护。开发团队很高兴,系统对于最初的客户来说运行得很好。
-
当销售团队将系统出售给一家大公司时,问题就开始了,声称它具有日历集成、电子邮件通知和令人惊叹的报告生成器。销售完成后,这些功能必须快速实施。
-
日历集成: 团队必须与 Google 日历和 Outlook 集成。不同的开发人员实施了解决方案,导致方法不一致。
-
电子邮件通知:接下来添加了电子邮件通知。一位开发人员使用特定的库,而另一位开发人员创建了自定义解决方案。混合的方法使代码变得混乱。
-
报告生成器: 对于报告生成器,开发人员使用了各种技术:PDF、Excel 导出和交互式仪表板。缺乏统一的方法使维护成为一场噩梦。
-
不断增长的复杂性:每个功能都是独立快速开发的,导致功能之间存在依赖关系。开发人员开始创建“快速修复”以使一切正常运行,从而增加了系统的复杂性和耦合性。
软件开发不是凭空发生的;各种内部和外部因素都会对其产生影响。我们都曾经历过或将会经历过这样的情况。
结局的开始
然后问题就开始了:
- 系统某一部分的变化意外地影响了其他部分。
- 小改动需要修改许多其他文件,使得估计变得困难。
- 月复一月,代码变得越来越难以理解,通常通过反复试验来修复。
- 生产力下降,每个人都害怕维护任务。
- 不可避免地呼吁“我们需要重构。”
- 某些任务只能由特定的开发人员处理(经典)
- 随着时间的推移,曾经编写精美且文档齐全的软件变成了火车残骸。
命名症状
很明显,我们现在拥有一个复杂的系统。
现在让我们“剖析”这种复杂性,以便更容易识别和减轻它。
嗯,“缓解”的意思是:
“减轻严重性、严重性或痛苦;减轻。”
我相信复杂性通常是代码所固有的。有些事情本质上是复杂的。作为开发人员,您的角色不仅仅是创建计算机可以高效执行的代码,还要创建未来的开发人员(包括未来的您)可以使用的代码。
“控制复杂性是计算机编程的本质。”
— 布莱恩·科尼汉
上述书籍的作者指出,复杂性通常以三种方式表现出来,我们将在这里进行探讨。
改变放大
当看似简单的更改需要在许多不同的地方进行修改时,就会发生更改放大。
例如,如果产品负责人请求“优先级”或“完成日期”字段,并且您的实体紧密耦合,那么您需要进行多少更改?
认知负荷
认知负荷是指开发人员完成任务所需的知识量和时间。
想象一下这样的场景:一位新开发人员加入了团队,他被指派修复报告生成器中的错误。为了完成此任务,开发人员需要:
- 了解不同的日历集成(Google 和 Outlook)。
- 掌握电子邮件通知的不同方法。
- 浏览报告生成器的碎片代码,处理 PDF、Excel 和仪表板。
- 整合这些不同的技术和风格来查找并修复错误。
这是典型的“无法估计”场景,任务可能需要 1 分或 8 分——最好掷 D20 并做出相应的反应。
未知的未知
未知的未知是当你不知道你不知道的事情时。
这是复杂性最糟糕的表现,因为你可能会改变不应该改变的东西,导致一切都崩溃。
示例:开发人员修改了电子邮件发送代码以添加新通知,但没有意识到这会影响依赖于该函数的报告生成器。这给客户带来了重大问题,体现了紧急复杂性的最坏形式。
复杂性的原因
看完恐怖故事和三个主要症状,让我们看看是什么导致了复杂性。
1. 依赖关系
依赖关系在软件中是必不可少的,并且无法完全消除。它们允许系统的不同部分相互作用并一起运行。然而,如果管理不当,依赖关系会显着增加复杂性。
定义:
当代码无法单独理解或修改时,就存在依赖关系,需要考虑或修改相关代码。
依赖项类型:
- Direct: 模块A直接依赖模块B。
- 传递性: 模块 A 依赖于模块 B,模块 B 又依赖于模块 C。
- 循环: 模块A、B、C以循环方式相互依赖。
2. 默默无闻
当重要信息不明显时,就会出现模糊性。这可能会使代码库难以理解,从而导致认知负荷增加和未知未知的风险。
定义:
当重要信息不明显时,就会出现模糊性。
晦涩难懂的例子:
- 不良命名:名称不明确的变量和函数。
- 隐藏的副作用:执行意外操作的方法。
- 全局状态: 过度使用全局变量。
- 深度继承: 行为分布在类层次结构中的多个级别。
请记住:复杂性是渐进的
- 复杂性很少是由单个“错误”或错误的决定引起的。
- 随着时间的推移,错误的决策和依赖性会“缓慢”地增加复杂性。
因为它是增量的,所以很容易想到,“就这一次,没关系。”但积累起来后,仅修复一两个依赖项并不会产生太大影响。
“软件工程中的一切都是权衡。”
——我不记得作者了
结论
我可以写很多你可能已经在互联网上看到的关于如何避免复杂性的规则、策略和框架:SOLID、设计模式、YAGNI、KISS 等。
但是,您可以将它们统一为一个指导原则(如“务实的程序员”中提到的。): “我正在实现的内容容易更改吗?” 如果答案是否定的,那么您可能会增加复杂性。
确保您的代码易于更改可以简化维护,减少开发人员的认知负担,并使系统更具适应性且不易出错。
谢谢你!