是时候进入软件架构了。在之前已经介绍了模块/类、组件的相关内容,这一部分会重点介绍软件架构的基本定义。
写在最前面
🌟软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,如果不亲身承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。
什么是软件架构?
软件架构工作的实质就是讨论规划如何将系统切分成组件,并安排好组件之间的排列关系与通信方式,即确定边界。
软件架构设计的目的一般有两个:
- 为了在工作中更好的对这些组件进行研发、部署、运行以及维护;
- 如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间地保留尽可能多的可选项;
一个软件系统的架构质量和该系统是否能正常工作(行为)的关系并不大,毕竟世界上有很多架构设计糟糕但是工作正常的软件系统。真正的麻烦往往会出现在这个软件系统的开发、部署以及后续的补充开发中。这也间接反映的作者的观点:软件系统的架构价值在一定程度上大于其行为价值。
软件架构设计的目标如下:
- 核心目标:一个良好的架构设计应该围绕着用例来展开,良好的架构设计应该只关注用例,并能将它们与其他的周边因素隔离,隔离的方式是尽可能推迟选择,保留更多的可选项;
- 主要目标:支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署;
- 终极目标:最大化程序员的生产力(解放生产力),同时最小化系统的总运营成本;
软件架构的设计重点是将策略彼此分离,然后将它们按照变更的方式进行重新分组,也就是划分出边界,在这一部分可以依据的原则包括SRP、CCP、CSP,SDP、SAP等等。
软件架构的职责
软件架构具有开发、部署、运行以及维护多个方面的职责。
在开发上:软件架构要方便软件系统的开发,因此不同的团队应该使用不同的软件架构设计。这在一定程度上也体现了康威定律。
为什么康维定律能够体现团队的架构?看视频了解更多:康威定律:为什么你的架构会反映团队结构?_哔哩哔哩_bilibili
在部署上:软件架构要实现软件的一键式轻松部署;
在运行上,
- 正如前面提到的,架构对于软件系统的高效运行远小于其他几个方面,作者认为这主要源于增加硬件资源可以弥补架构在运行方面的考虑不足,而架构设计通常需要更为昂贵的人力资源;
- 除了架构设计对于高效运行的作用,架构还应该能反应系统在运行时的需求,也就是说,设计良好的系统架构应该可以使开发人员对系统的运行过程一目了然。架构应该起到揭示系统运行过程的作用,架构应该将系统中的用例、功能以及该系统的必备行为设置为对开发者可见的一级实体,简化它们对于系统的理解,《尖叫的软件架构》一章就是在重点说明这个问题;
在维护上,软件系统维护的成本一般是最高的,维护的成本可以分为探秘与风险两类:
- 探秘:探秘(spelunking)的成本主要来自我们对于现有软件系统的挖掘,目的是确定新增功能或被修复问题的最佳位置和最佳方式;
- 风险:风险(risk),则是指当我们进行上述修改时,总是有可能衍生出新的问题,这种可能性就是风险成本;
保持可选项
回忆之前提到的软件架构提到的两种价值:“行为价值”和“架构价值”,提升架构价值的方式就是让软件更软,而让软件更软的方式就是让软件架构尽可能多的保持可选项。
软件系统的元素分为策略与细节两类,策略体现的是软件中所有的业务规则与操作过程,因此它是系统的真正价值所在,细节则是指那些让操作该系统的人、其他系统以及程序员们与策略进行交互,但是又不会影响到策略本身的行为。如 IO 设备、数据库等等,细节选项应该是尽可能保留的。
软件架构师的目标就是要创建一种系统形态,该形态会以策略为最基本的元素,并让细节与策略脱离关系,以允许在具体决策过程中推迟或延迟与细节相关的内容。因为越到项目的后期,我们就拥有越多的信息来做出合理的决策。一个优秀的软件架构师应该致力于最大化可选项的数量。
作者使用设备无关性的例子来说明推迟细节的重要性。现代操作系统的输入输出设备种类多样,而在计算机发展的最初时期,打孔纸带是主流(几乎是唯一)的一种输入输出方式,编程人员很自然的就将读取/输出纸带的代码耦合到了系统代码当中,当磁带出现后,开发人员不得不开发新的代码来适配磁带输入输出,光盘出现后…为了应对这种重复开发,适应性差的问题,开发人员提出了设备无关行的概念,即将设备抽象成函数,OS会利用函数与输入输出设备交互,而开发人员只需要提供这些函数的具体实现。此时输入输出就成为了OS的插件,这也是开闭原则的雏形(拥抱👏新增、抗拒🥊修改!)。
保持独立性
良好的软件架构应该有充足的独立性。
解耦可以让我们保证软件架构的独立性。
解耦分为水平解耦与垂直解耦。
- 水平解耦(按层解耦):系统可以被解耦成若干个水平分层——UI、数据库等等;
- 用例解耦(垂直解耦):在水平分层解耦的同时,也按用例将其切分成多个垂直分片,如:将增加订单的用例UI与删除订单的用例UI分开。
如果我们按照变更原因的不同对系统进行解耦,就可以持续地向系统内添加新的用例,而不会影响旧有的用例。
同样,解耦的层次也可以是不同的。可以在源码、部署以及服务三个不同的层次来完成解耦。
- 源码层次:控制源码中模块之间的依赖关系,系统的组件通过函数调用来进行交互。这种模式叫做单体结构;
- 部署层次:控制部署单元(如 Jar 文件)之间的依赖关系,系统的组件可能使用跨线程(注意不是跨网络)的通信、Socket 通信或共享内存融通信;
- 服务层次:将组件间的依赖降低到数据结构级别,然后通过网络数据包进行通信,如微服务通过 rpc、rest 交互。
并没有严格的标准说明那个解耦层次是更好的,因为随着项目的逐渐成熟,最好的解耦模式可能会发生变化。一个设计良好的架构应该能允许一个系统从单体结构开始,以单一文件的形式部署,然后逐渐成长为一组相互独立的可部署单元,甚至是独立的服务或者微服务。最后还能随着情况的变化,允许系统逐渐回退到单体结构。一个设计良好的架构在上述过程中还应该能保护系统的大部分源码不受变更影响。对整个系统来说,解耦模式也应该是一个可选项。我们在进行大型部署时可以采用一种模式,而在进行小型部署时则可以采用另一种模式。
在了解解耦之后,让我们看看解耦对系统独立性到底有何影响。
解耦对系统运行独立性的意义
如果不同面向之间的用例得到了良好的隔离,那么需要高吞吐量的用例就和需要低吞吐量的用例互相自然分开了。如果 UI 和数据库的部分能从业务逻辑分离出来,那么它们就可以运行在不同的服务器上。而且需要较大带宽的应用也可以在多个服务器上运行多个实例。
解耦对系统开发独立性的意义
只要系统按照其水平分层和用例进行了恰当的解耦,整个系统的架构就可以支持多团队开发,不管团队组织形式是分功能开发、分组件开发、分层开发,还是按照别的什么变量分工都可以
解耦对系统部署独立性的意义
如果解耦工作做得好,我们甚至可以在系统运行过程中热切换(hot-swap)其各个分层实现和具体用例。在这种情况下,我们增加新用例就只需要在系统中添加一些新的jar文件,或启动一些服务即可,其他部分将完全不受影响
什么是代码重复
代码中的重复可以分为两种:
- 真正的重复:代码中需要被消灭的部分;
- 虚假的重复:看起来重复的代码,实际上是不同的演进路径,也就是拥有不同的变更速率与变更边缘。CRP阐述:对于使用频次不同的代码需要拆分到不同的组件中,如果在阅读代码时没有考虑到这一点就很有可能误认为虚假的重复为重复代码,这些“重复”有时候是必须的,还记得计算工资的例子吗?
(第五篇完)