对抗复杂度的圣杯战争:软件架构究竟该如何设计?

   熵增定律

熵的概念最早起源于物理学,用于度量一个热力学系统的无序程度。不幸的是,热力学法则决定了宇宙中的熵会趋向最大化。虽然软件开发不受绝大多数物理法则的约束,但我们无法躲避来自熵增加的重击。当软件中的无序化增加时,程序员会说“软件在腐烂”。有些人可能会用更乐观的术语来称呼它,即“技术债”,潜台词是说它们总有一天会偿还的——恐怕不会还了。

   破窗效应

破窗效应(英语:Broken windows theory)是犯罪学的一个理论, 此理论认为环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。以一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的破窗效应。software = soft + ware软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求 。--《架构整洁之道》对于每个软件系统,我们都可以通过行为架构两个维度来体现它的实际价值。软件研发人员应该确保自己的系统在这两个维度上的实际价值都能长时间维持在很高的状态。

   行为价值

软件系统的行为是其最直观的价值维度 。程序员的工作就是让机器按照某种指定方式运转,给系统的使用者创造或者提高利润 。行为价值 = 按照需求文档编写代码,并且修复任何 Bug。需求转化为代码。

   架构价值

软件发明的目的,就是让我们可以以一种灵活的方式来改变机器的工作行为 。对机器上那些很难改变的工作行为,我们通常称之为硬件 ( hardware ) 。为了达到软件的本来目的,软件系统必须够 “软”一一也就是说,软件应该容易被修改。当需求方改变需求的时候,随之所需的软件变更必须可以简单而方便地实现 。架构价值 = 灵活、低成本。上图:面条式代码,不利于灵活修改。

   哪个价值更重要

上图:艾森豪威尔矩阵。软件系统的第一个价值维度:系统行为,是紧急的,但是并不总是特别重要。软件系统的第二个价值维度:系统架构,是重要的,但是并不总是特别紧急。软件架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。软件架构师这一职责本身就应更关注系统的整体结构,而不是具体的功能和系统行为的实现。软件架构师必须创建出一个可以让功能实现起来更容易、修改起来更简单、扩展起来更轻松的软件架构。请记住:如果忽视软件架构的价值,系统将会变得越来越难以维护, 终会有一天,系统将会变得再也无法修改。如果系统变成了这个样子,那么说明软件开发团队没有和需求方做足够的抗争, 没有完成自己应尽的职责。软件设计的核心在于降低复杂性---《软件设计的哲学》

   复杂性的定义

对抗复杂度的圣杯战争:软件架构究竟该如何设计?-每日运维系统的总体复杂度(C)由每个部分的复杂度(cp)乘以开发人员在该部分上花费的时间(tp)加权。在一个永远不会被看到的地方隔离复杂性几乎和完全消除复杂性一样好。子模块的复杂度 Cp 是一个经验值,它关注几个现象:▶︎ 变更放大:复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。▶︎ 认知负荷:复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。▶︎ 未知的未知:复杂性的第三个症状是,必须修改哪些代码才能完成任务,或者开发人员必须获得哪些信息才能成功地执行任务,这些都是不明显的。

   复杂性的原因

复杂性是由两件事引起的:依赖性和模糊性。依赖关系是软件的基本组成部分,不能完全消除。实际上,我们在软件设计过程中有意引入了依赖性。每次编写新类时,都会围绕该类的 API 创建依赖关系。但是,软件设计的目标之一是减少依赖关系的数量,并使依赖关系保持尽可能简单和明显。复杂性的第二个原因是晦涩。当重要的信息不明显时,就会发生模糊。一个简单的例子是一个变量名,它是如此的通用,以至于它没有携带太多有用的信息(例如,时间)。或者,一个变量的文档可能没有指定它的单位,所以找到它的唯一方法是扫描代码,查找使用该变量的位置。复杂性不是由单个灾难性错误引起的;它堆积成许多小块。单个依赖项或模糊性本身不太可能显著影响软件系统的可维护性。之所以会出现复杂性,是因为随着时间的流逝,成千上万的小依赖性和模糊性逐渐形成。最终,这些小问题太多了,以至于对系统的每次可能更改都会受到其中几个问题的影响。复杂性的增量性质使其难以控制。可以很容易地说服自己,当前更改所带来的一点点复杂性没什么大不了的。但是,如果每个开发人员对每种更改都采用这种方法,那么复杂性就会迅速累积。一旦积累了复杂性,就很难消除它,因为修复单个依赖项或模糊性本身不会产生很大的变化。

   拒绝战术编程

战术编程致力于完成任务,新增加特性或者修改 Bug 时,能解决问题就好。这种工作方式,会逐渐增加系统的复杂性。如果系统复杂到难以维护时,再去重构会花费大量的时间,很可能会影响新功能的迭代。战略编程,是指重视设计并愿意投入时间,短时间内可能会降低工作效率,但是长期看,会增加系统的可维护性和迭代效率。

   奥卡姆剃刀原则

又被称作“简单有效原理”:“如无必要,勿增实体”。这个原理广泛应用于哲学、科学、管理学、经济学等等众多领域。在软件架构/软件开发领域, 该原则同样适用:less is more, simple is best。

   一致性

一致性是降低系统复杂性并使其行为更明显地强大工具。如果系统是一致的,则意味着相似的事情以相似的方式完成,而不同的事情则以不同的方式完成。一致性会产生认知影响力:一旦你了解了某个地方的工作方式,就可以使用该知识立即了解其他使用相同方法的地方。如果系统的实施方式不一致,则开发人员必须分别了解每种情况。这将花费更多时间。尤其对于一个大规模系统,往往需要多个团队共同开发完成,如果不遵循一致原则,就会导致整个平台的建设缺乏完整性和规范性,各个子系统各自为政,业务功能重复开发,技术实现五花八门,服务集成复杂低效,信息冗余制造出知识壁垒。一致性包括各个方面, 主要包括:架构,技术选型,代码规范,流程,机制,工具,平台,解决方案一致,思考问题的角度等。

   正交性

“正交性”是从几何学中借来的术语。如果两条直线相交成直角,它们就是正交的,比如图中的坐标轴。用向量术语说,这两条直线互不依赖。沿着某一条直线移动,你投影到另一条直线上的位置不变。在计算技术中,该术语用于表示某种不相依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。在设计良好的系统中,数据库代码与用户界面是正交的:你可以改动界面,而不影响数据库;更换数据库,而不用改动界面。正交的好处:▶︎ 提高生产率:改动得以局部化,促进复用 M+N--> M*N。▶︎ 降低风险:问题隔离,更容易测试。▶︎ 团队的正交:康威定律,如果团队的组织有许多重叠,各个成员就会对责任感到困惑。每一次改动都需要整个团队开一次会,因为他们中的任何一个人都可能受到影响。

   可逆性

自世纪之交以来,我们看到了以下服务端架构的“最佳实践”:▶︎ 大铁块 ;▶︎ 大铁块的联合;▶︎ 带负载均衡的商用硬件集群;▶︎ 将程序运行在云虚拟机中;▶︎ 将服务运行在云虚拟机中;▶︎ 把云虚拟机换成容器再来一遍;▶︎ 基于云的无服务器架构;▶︎ 最后,无可避免地,有些任务又回到了大铁块。 你能为这种架构的变化提前准备吗?做不到。你能做的就是让修改更容易一点。将第三方 API 隐藏在自己的抽象层之后。将代码分解为多个组件:即使最终会把它们部署到单个大型服务器上,这种方法也比一开始做成庞然大物,然后再切分要容易得多。---《程序员修炼之道》

   DRY(Don't repeat yourself)

重复是软件的原罪之一,“Dont repeat yourself” 告诉我们应该尽可能地消灭重复和冗余。重复使软件的阅读,修改,测试变得复杂,消灭重复,是使软件变得简单的手段之一。DRY 不只是代码的重复, 还包括以下任何你能想到的方面:▶︎ 数据定义。▶︎ API重复。▶︎ 人员重复。▶︎ 代码与文档之间的重复。▶︎ 代码与注释之间的重复。▶︎ 工具重复。▶︎ 服务重复。

   设计两次

设计软件非常困难,因此你对如何构造模块或系统的初步思考不太可能会产生最佳的设计。如果为每个主要设计决策考虑多个选项,最终将获得更好的结果:设计两次。大型软件的设计已经复杂到没人能够一次就想到最佳方案,一个仅仅“可行”的方案,可能会给系统增加额外的复杂性。

   分层与抽象

软件系统由不同的层次组成,层次之间通过接口来交互。在严格分层的系统里,内部的层只对相邻的层次可见,这样就可以将一个复杂问题分解成增量步骤序列。由于每一层最多影响两层,也给维护带来了很大的便利。分层系统最有名的实例是 TCP/IP 网络模型。在分层系统里,每一层应该具有不同的抽象。TCP/IP 模型中,应用层的抽象是用户接口和交互;传输层的抽象是端口和应用之间的数据传输;网络层的抽象是基于 IP 的寻址和数据传输;链路层的抽象是适配和虚拟硬件设备。如果不同的层具有相同的抽象,可能存在层次边界不清晰的问题。

   复杂性下沉

软件架构设计本身就是一门划分边界的艺术 。边界的作用是将软件分割成各种元素,以便约束边界两侧之 间的依赖关系。

架构师们所追求的目标是最大限度地降低构建和维护一个系统所需的人力资源。系统最消耗人力资源的是系统中存在的耦合——尤其是那些过早做出的、不成熟的决策(与系统的业务需求,也就是用例无关)所导致的耦合。

需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。然后通过对源代码的修改,让这些非核心组件依赖于系统的核心业务逻辑组件。

通过划清边界,我们可以推迟和延后一些细节性的决策,这最终会为我们节省大量的时间、避免大量的问题。这就是一个设计良好的架构所应该带来的助益。

   通信与集成

  • 防腐层 ACL

防腐层其实是设计思想“间接”的一种体现,引入一个中间层,有效隔离限界上下文之间耦合 防腐层经常扮演:适配器、调停者、外观等角色(设计模式中常见几种结构型模式) 防腐层往往属于下游限界上下文,用以隔绝上游限界上下文可能发生的变化。

对付遗留系统时,防腐层可谓首选利刃。

  • 开放主机服务 OHS

你可以使用框架,只是不要耦合它。保持在疏远的距离(Keep it at arm’s length)。将框架视为属于体系结构外围的一个细节。不要让它进入内部圈子。

如果框架要你从它的基类派生你的业务对象,说不!代之以派生代理,并将这些代理保留在作为业务规则插件的组件中。

不要让框架进入你的核心代码。相反,按照依赖规则将它们集成到组件中,组件再插入(plug in)核心代码的。

统一建模语言(Unified Modeling Language,UML)是一种面向对象的通用建模语言,可以用于对软件密集型系统的制品进行可视化、详述、构造和文档化。

UML 可以方便技术人员之间进行方案沟通, 技术传承,可以挑选常用的几个进行学习。

▶︎ 用例图

▶︎ 类图

▶︎ 流程图(泳道图)

▶︎ 时序图

▶︎ 状态图

来源:腾讯开发者