软件开发的复杂性及应对策略

1 复杂度的成因

决定软件系统复杂度的3个因素:规模,结构和不可预测的变化。

1.1 规模

影响理解力的第一要素是规模。软件的需求决定了系统的规模。当需求呈现线性增长的趋势时,为了实现这些功能,软件规模也会以近似的速度增长。由于需求不可能做到完全独立,导致出现相互影响相互依赖的关系,修改一处就会牵一发而动全身。

1.2 结构

相似而回旋繁复的结构使得本来封闭狭小的空间被魔法般地扩展为一个无限的空间,变得无穷大,仿佛这空间被安置了一个循环,倘若没有找到正确的退出条件,循环就会无休无止,永远无法退出。许多规模较小却格外复杂的软件系统,就好似这样的一座迷宫。

此时,结构成了决定系统复杂度的关键因素。

结构之所以变得复杂,在多数情况下还是因为系统的质量属性决定的。例如,我们需要满足高性能、高并发的需求,就需要考虑在系统中引入缓存、并行处理、CDN、异步消息以及支持分区的可伸缩结构。倘若我们需要支持对海量数据的高效分析,就得考虑这些海量数据该如何分布存储,并如何有效地利用各个节点的内存与 CPU 资源执行运算。

无论是优雅的设计,还是拙劣的设计,都可能因为某种设计权衡而导致系统结构变得复杂。唯一的区别在于前者是主动地控制结构的复杂度,而后者带来的复杂度是偶发的,是错误的滋生,是一种技术债,它可能会随着系统规模的增大而导致一种无序设计。

我们看一个无序设计的软件系统,就好像隔着一层半透明的玻璃观察事物一般,系统中的软件元素都变得模糊不清,充斥着各种技术债。细节层面,代码污浊不堪,违背了“高内聚、松耦合”的设计原则,导致许多代码要么放错了位置,要么出现重复的代码块;架构层面,缺乏清晰的边界,各种通信与调用依赖纠缠在一起,同一问题域的解决方案各式各样,让人眼花缭乱,仿佛进入了没有规则的无序社会。

1.3 变化

未来总会出现不可预测的变化,这种不可预测性带来的复杂度,使得我们产生畏惧,因为我们不知道何时会发生变化,变化的方向又会走向哪里,这就导致心理滋生一种仿若失重一般的感觉。变化让事物失去控制,受到事物牵扯的我们会感到惶恐不安。

在设计软件系统时,变化让我们患得患失,不知道如何把握系统设计的度。若拒绝对变化做出理智的预测,系统的设计会变得僵化,一旦变化发生,修改的成本会非常的大;若过于看重变化产生的影响,渴望涵盖一切变化的可能,一旦预期的变化不曾发生,我们之前为变化付出的成本就再也补偿不回来了。这就是所谓的“过度设计”。

从需求的角度讲,变化可能来自业务需求,也可能来自质量属性。

如果变化是不可预测的,那么软件系统也会变得不可预测。一方面我们要尽可能地控制变化,至少要将变化产生的影响限制在较小的空间范围内;另一方面又要保证系统不会因为满足可扩展性而变得更加复杂,最后背上过度设计的坏名声。

2 应对策略

2.1 分而治之,控制规模

针对规模带来的复杂度,我们应注意克制做大、做全的贪婪野心,尽力保证系统的小规模。简单说来,就是分而治之的思想,遵循小即是美的设计美学。

Unix 的设计哲学被总结为以下两条:

Make each program do one thing well. To do a new job, build a fresh rather than complicate old programs by adding new “features.”

Expect the output of every program to become the input to another, as yet unknown, program.

这两条原则是相辅相成的。第一条原则要求一个程序只做一件事情,符合“单一职责原则”,在应对新需求时,不会直接去修改一个复杂的旧系统,而是通过添加新特性,然后对这些特性进行组合。要满足小程序之间的自由组合,就需要满足第二条原则,即每个程序的输入和输出都是统一的,因而形成一个统一接口(Uniform Interface),以支持程序之间的自由组合(Composability)。利用统一接口,既能够解耦每个程序,又能够组合这些程序,还提高了这些小程序的重用性,这种“统一接口”,其实就是架构一致性的体现。

2.2 保持结构的清晰与一致

所有设计质量高的软件系统都有相同的特征,就是拥有清晰直观且易于理解的结构。

Robert Martin 分析了这么多年诸多设计大师提出的各种系统架构风格与模式,包括 Alistair Cockburn 提出的六边形架构(Hexagonal Architecture),Jeffrey Palermo 提出的洋葱架构(Onion Architecture),James Coplien 与 Trygve Reenskaug 提出的 DCI 架构,Ivar Jacobson 提出的 BCE 设计方法。结果,他认为这些方法的共同特征都遵循了“关注点分离”架构原则,由此提出了整洁架构的思想。

整洁架构提出了一个可测试的模型,无需依赖于任何基础设施就可以对它进行测试,只需通过边界对象发送和接收对应的数据结构即可。它们都遵循稳定依赖原则,不对变化或易于变化的事物形成依赖。整洁架构模型让外部易变的部分依赖于更加稳定的领域模型,从而保证了核心的领域模型不会受到外部的影响。

整洁架构的目的在于识别整个架构不同视角以及不同抽象层次的关注点,并为这些关注点划分不同层次的边界,从而使得整个架构变得更为清晰,以减少不必要的耦合。要做到这一点,则需要合理地进行职责分配,良好的封装与抽象,并在约束的指导下为架构建立一致的风格,这是许多良好系统的设计特征。

一些具体的技术手段:

隔离业务复杂度与技术复杂度

要避免业务逻辑的复杂度与技术实现的复杂度混淆在一起,首要任务就是确定业务逻辑与技术实现的边界,从而隔离各自的复杂度。业务逻辑并不关心技术是如何实现的,无论采用何种技术,只要业务需求不变,业务规则就不会发生变化。换言之,在理想状态下,我们应该保证业务规则与技术实现是正交的。

分层架构的关注点分离

分层架构遵循了“关注点分离”原则,将属于业务逻辑的关注点放到不同的架构层。

六边形架构的内外分离

由 Cockburn 提出的六边形架构则以“内外分离”的方式,更加清晰地勾勒出了业务逻辑与技术实现的边界,且将业务逻辑放在了架构的核心位置。

2.3 拥抱变化

Kent Beck给出的答案是:软件设计是为了在让软件在长期范围内容易应对变化

在这个精炼的定义中,包含着三个关键词:长期容易变化。这意味着:

越是需要长期维护的项目,变化更多,也更难预测变化的方式;

软件设计,事关成本;

如何在难以预测的千变万化中,保持低廉的变更成本,正是软件设计要解决的问题。

对此,Kent Beck提出了一个更为精炼的原则:局部化影响

一个易于应对变化的软件设计应该遵从高内聚,低耦合原则。让实际发生的需求变化来驱动我们识别变化,管理变化,从而让我们的系统达到恰如其分的内聚度和耦合度:

策略一:消除重复

对于完全重复的代码进行消除,合二为一,会让系统更加高内聚、低耦合

而更为关键的是:如果两个模块之间是部分重复的,则发出了一个重要的信号:这两个模块都至少存在两个变化原因,或两重职责。站在系统的角度看,它们之间存在着不变的部分(即重复的部分);也存在变化的部分(即差异的部分)。这意味着这两个模块都存在两个变化原因。

策略二:分离不同的变化方向

分离不同变化方向,目标在于提高内聚度。因为多个变化方向,意味着一个模块存在多重职责。将不同的变化方向进行分离,也意味着各个变化方向职责的单一化。此策略的应用时机也非常明确:当你发现需求导致一个变化方向出现时,将其从原有的设计中分离出去。


策略三:缩小依赖范围

两个模块之间并不存在耦合,它们的都共同耦合在API上。因而 API如何定义才能降低耦合度,才是我们应该关注的重点。

对于API定义所带来的耦合度影响,需要遵循如下原则:

首先,客户实现模块的数量,会对耦合度产生重大的影响。它们数量越多,意味着 API 变更的成本越高,越需要花更大的精力来仔细斟酌。

其次,对于影响面大的API(也意味着耦合度高),需要使用更加弹性的API定义框架,以有利于向前兼容性。

而具体到策略缩小依赖范围,它强调:

API 应包含尽可能少的知识。因为任何一项知识的变化都会导致双方的变化;

API 也应该高内聚,而不应该强迫API的客户依赖它不需要的东西。


策略四:向着稳定的方向依赖

究竟什么样的API更倾向于稳定?不难知道,站在What,而不是How的角度;即

站在需求的角度,而不是实现方式的角度定义API,会让其更加稳定。

而需求的提出方,一定是客户端,而不是实现侧。这就意味着,我们在定义接口时,应该站在客户的角度,思考用户的本质需要,由此来定义API。而不是站在技术实现的方便程度角度来思考API定义。

而这正是封装信息隐藏的关键。


SOLID 原则是面向对象编程的设计原则。

https://www.jianshu.com/p/d127b8afc8cb

https://www.jianshu.com/p/f7f5813882a1

引用文章

https://gitbook.cn/gitchat/column/5cdab7fb34b6ed1398fd8de7

https://www.jianshu.com/p/d127b8afc8cb

https://www.jianshu.com/p/f7f5813882a1

延伸阅读

六边形架构:

https://www.infoq.cn/article/2014/11/exploring-hexagonal-architecture

https://juejin.im/post/5a9d2943518825556a71e347

https://insights.thoughtworks.cn/port-and-adapter-architecture/

整洁架构:

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 第六章 开发方法 6.1 软件生命周期 软件生命周期划分为8个阶段:可行性研究与计划、需求分析、概要设计、详细设计...
    步积阅读 5,309评论 0 3
  • 最近再看阮一峰的一篇博客提到了一本书《Software Architecture Patterns》(PDF),写...
    卓_然阅读 12,386评论 0 22
  • 一、生命周期 一个事物一旦出生,就必然会长大,变异,一旦长大,就面临着衰老,接下来就是消亡了,这个过程就称为一个事...
    ZyBlog阅读 7,588评论 1 11
  • 前段时间有报道称,有学者质疑“大数据”理论,也有硅谷公司负责人质疑大数据应用的效果。结合2011年Gartner关...
    栀子花_ef39阅读 5,259评论 0 5
  • 一如从前 春来抱恙病体缠, 渐恋床榻被衾眠。 幸亏惊蛰天回暖, 方才痊愈如从前。
    往事如烟胖婆婆阅读 3,558评论 1 15