16 Independence

在我们先前的讲述中,一个好的架构必须支持:
1.系统的用例与运行方式
2.系统维护
3.系统开发
4.系统部署

使用案例:

第一点 ——用例—— 意味着系统架构必须支撑系统的核心设计意图。如果这是一个购物车应用,那么架构就必须支持购物车相关的用例。事实上,这是架构师首先要考虑的问题,也是架构设计的第一优先级:架构必须为用例服务。
不过,正如我们之前讨论的,架构对系统具体行为的影响其实并不大,能留给行为的灵活空间也很有限。但影响力并非全部。好的架构在支撑行为方面,最重要的作用是清晰地呈现和暴露行为,让系统的核心意图在架构层面一目了然。
一个拥有良好架构的购物车应用,看上去就应该像一个购物车应用。系统的用例会在结构中清晰可见,开发者不必费力去寻找各种行为逻辑,因为这些逻辑本身就是系统顶层的一等公民。它们会是以类、函数或模块的形式出现在架构中的显眼位置,并且拥有能清晰描述其功能的命名。

Operation:

架构在支撑系统运行方面所扮演的角色,远比表面上更实质、更关键。如果系统需要每秒处理 10 万名用户,架构就必须在对应用例上满足所需的吞吐量和响应时间。如果系统必须在毫秒级查询大数据立方体,那么架构的结构就必须支持这类操作。

对某些系统而言,这意味着要把处理单元组织成一系列小型服务,在多台服务器上并行运行。对另一些系统,则意味着在单个处理器的单个进程内,使用大量轻量级线程共享地址空间。还有些系统只需要少数运行在独立地址空间的进程即可。甚至有些系统作为单进程的简单单体程序就能正常运行。

听起来可能有些反直觉,但优秀的架构师会把这类运行方式的决策作为可选项保留下来。一个写成单体结构、并且强依赖这种单体结构的系统,一旦后续需要,很难平滑升级为多进程、多线程或微服务架构。

相比之下,如果架构能保持组件之间恰当的隔离,并且不对组件间的通信方式做任何预设,那么随着系统运行需求的变化,它就能更轻松地在线程、进程、服务等各种模式之间切换演进。

Development:

架构在支撑开发环境方面同样扮演着重要角色,这也是 ** 康威定律(Conway’s Law)** 发挥作用的地方。康威定律指出:任何设计系统的组织,所产出的设计结果,其结构都会复刻该组织内部的沟通结构。
如果一个系统必须由拥有多个团队、多种关注点的组织来开发,那么它的架构就必须支持各个团队独立工作,让团队在开发过程中互不干扰。这一点可以通过合理划分系统来实现:将系统拆分成隔离良好、可独立开发的组件,再把这些组件分配给不同团队,让它们彼此互不影响地并行工作。

Deployment:

架构在决定系统部署难易程度上同样起着至关重要的作用。部署的目标是“一键即刻部署”

好的架构不依赖一大堆零散的配置脚本和反复修改的属性文件,也不需要手动创建目录、文件并小心翼翼地摆放到位。好的架构能让系统在构建完成后,直接就能部署运行

同样,这一点也是通过合理划分和隔离系统组件来实现的,包括那些统筹全局的核心组件——它们负责把整个系统串联起来,确保每个组件都能被正确启动、集成和管理。

Leaving Options Open:

好的架构会通过组件结构平衡所有这些诉求,让它们彼此兼容、同时得到满足。听起来很简单,对吧?嗯,这话我写起来确实容易。

但现实是,想要达到这种平衡非常困难。问题在于,大多数时候我们根本不知道所有的用例,也不清楚运行约束、团队结构,或是部署要求。更糟的是,就算我们一开始知道,随着系统进入生命周期,这些东西也必然会变。简单说:我们必须达成的目标,既模糊又善变。欢迎来到真实世界。

但也并非毫无办法:有一些架构原则实现起来成本不高,却能帮我们平衡这些诉求——哪怕你对目标还没有清晰的图景。这些原则能帮我们把系统拆分成高度隔离的组件,让我们能尽可能久地保留更多选择

好的架构,核心就是保留选择余地,让系统在所有需要变化的地方,都能轻松地被修改

Decoupling Layers:

先思考用例。架构师希望系统结构能够支持所有必要的用例,但他并不知道所有用例具体是什么。不过,架构师确实知道系统的核心定位—— 它是购物车系统、物料清单系统,还是订单处理系统。

因此,架构师可以运用单一职责原则(SRP)和共同闭包原则(CCP),在系统核心定位的前提下:
把因不同原因而变化的模块分离开;把因相同原因而变化的模块聚合在一起。

哪些东西会因为不同原因而变化?有一些很明显:用户界面的变更原因,和业务规则完全无关。用例则同时包含界面和规则两部分。

显然,一个优秀的架构师会希望把用例中的 UI 部分与业务规则部分分开,使它们可以独立修改,同时又让用例本身清晰可见、一目了然。
业务规则本身,有的和应用紧密相关,有的则更通用。例如:输入字段的校验,是和应用强相关的业务规则;账户利息计算、库存盘点,则是更偏领域层面的业务规则。

这两类规则的变更频率和原因都不同,因此应该被分开,以便独立修改。
数据库、查询语言,甚至数据表结构,都只是技术细节,和业务规则、用户界面无关。它们的变更节奏和原因也独立于系统其他部分。因此,架构也应该将它们与系统其他部分解耦,使其可以独立变化。
如此一来,我们就会把系统拆分为解耦的水平分层—— 至少包括:UI 层、应用特定业务规则层、应用无关业务规则层、数据库层,等等。

Decoupling Use Cases:

还有那些东西不会因为不同的原因而变化?
答案是:用例本身!
在订单录入系统中,新增订单的用例,其变更频率和原因,几乎肯定和删除订单的用例不一样。
用例本身,就是一个非常自然的系统划分方式。同时用例是纵向的细窄切片,会贯穿系统的各个水平分层。每个用例都会用到一部分UI,一部分应用到特定业务规则,一部分与应用无关的业务规则,以及一部分数据库功能。所以在我们把系统水平分层的同时,也要把系统按照用例纵向切分,让这些纵向切片贯穿所有的水平层。

为了实现这种解耦,我们会:
把 “新增订单” 用例的 UI,和 “删除订单” 用例的 UI 分开;
业务规则、数据库访问也照此分开;
在整个系统的纵向上,让各个用例彼此保持独立。
你应该能看出这里的模式了:只要把系统中因不同原因而变化的部分解耦,后续新增用例时,就不会影响到老用例。

Decoupling Mode:

现在再来想想,这种解耦对第二点 ——系统运行—— 意味着什么。如果用例的各个部分已经被拆分,那么那些需要高吞吐量运行的部分,很可能已经和低吞吐量的部分分离开了。如果 UI 和数据库已经与业务规则解耦,它们就可以运行在不同的服务器上。对带宽要求更高的部分,还可以在多台服务器上做副本。
简单来说,为了用例而做的解耦,同时也服务于系统运行。不过,要真正发挥运行层面的优势,解耦必须采用合适的模式。想要在不同服务器上独立运行,被拆分的组件就不能依赖于同一个处理器的地址空间;它们必须是独立的服务,通过某种网络进行通信。
很多架构师根据一种模糊的代码量标准,把这类组件叫作 “服务” 或 “微服务”。事实上,基于服务的架构通常也被称为面向服务架构(SOA)。
如果这些术语让你心里警铃大作,别担心。我并不是要说 SOA 是最好的架构,也不是说微服务就是未来趋势。这里想表达的核心是:有时候,我们必须把组件拆分到服务级别。
记住:好的架构会保留选择余地。解耦的模式本身,就是其中一个可选项。
在进一步探讨这个话题之前,我们先看看另外两点。

独立可开发性

第三点是开发。很明显,当组件被充分解耦后,团队之间的相互干扰就会大幅减少。如果业务规则不依赖于用户界面,那么专注于 UI 的团队就不太会影响到专注于业务规则的团队。如果各个用例之间彼此解耦,那么负责新增订单用例的团队,也几乎不会干扰到负责删除订单用例的团队。

只要分层和用例是解耦的,系统架构就能适配团队组织方式——无论团队是按功能、按组件、按分层,还是其他形式划分。

独立可部署性

用例和分层的解耦,也为部署提供了极高的灵活性。事实上,如果解耦做得足够好,就可以在系统运行时热替换某个分层或用例。新增一个用例可能简单到只需要往系统中加入几个新的 jar 包或服务,而完全不用改动其他部分。

Dupilcation

架构师经常会掉进一个陷阱 —— 一个由对重复代码的恐惧造成的陷阱。
重复在软件中通常是件坏事。我们不喜欢重复代码。当代码是真正重复时,作为专业人士,我们有责任去减少并消除它。
但是,重复分很多种。一种是真正的重复:只要改了一处,所有重复的地方都必须跟着改。另一种则是虚假或偶然的重复:如果两段看起来重复的代码,后续演化路径完全不同 —— 变更频率不同、原因也不同 —— 那它们就不是真正的重复。过上几年再回头看,你会发现它们已经变得完全不一样。
现在想象两个用例,界面结构非常相似。架构师往往会忍不住想要复用这部分结构代码。但他们应该这么做吗?这是真正的重复,还是偶然重复?
极大概率是偶然重复。随着时间推移,这两个界面几乎一定会分道扬镳,最终变得完全不同。正因如此,必须小心避免过早把它们合并。否则,以后再想拆分就会非常痛苦。
当你纵向拆分用例时,一定会遇到这个问题:因为界面相似、算法相似、数据库查询或结构相似,就忍不住想把用例耦合在一起。一定要谨慎。抵抗住这种不假思索就消除重复的冲动。先确认重复是不是真的。
同样,当你横向分层时,可能会发现某条数据库记录的数据结构,和某个界面视图的数据结构非常像。你可能会想:干脆直接把数据库记录传给 UI 就行了,何必再建一个一模一样的视图模型,然后把数据复制过去呢?千万小心:这种重复几乎肯定是偶然重复。单独建一个视图模型成本并不高,却能帮你保持各层之间正确解耦。

Decoupling Modes (Again)

回到解耦模式。分层和用例可以通过多种方式解耦:可以在源码级别、二进制(部署)级别,以及执行单元(服务)级别进行解耦。
源码级别我们可以控制源码模块之间的依赖关系,使得修改一个模块时,不会强制其他模块发生变更或重新编译(例如 Ruby Gems)。在这种解耦模式下,所有组件都在同一个地址空间中运行,通过简单的函数调用相互通信。只有一个可执行文件加载到计算机内存中。人们通常称这种结构为单体架构。
部署级别我们可以控制 jar、DLL 或共享库这类可部署单元之间的依赖,使得一个模块的源码变更,不会强制其他模块重新构建和重新部署。许多组件仍然可能位于同一个地址空间,通过函数调用通信;另一些组件则可能在同一处理器的不同进程中,通过进程间通信、套接字或共享内存交互。关键点在于,解耦后的组件被划分为 jar、Gem、DLL 等可独立部署的单元。
服务级别我们可以将依赖降低到数据结构级别,仅通过网络数据包通信,使得每个执行单元在源码和二进制层面都完全独立于其他单元(例如服务或微服务)。
那么,哪种模式最好?
答案是:在项目早期,很难知道哪种模式最优。而且随着项目逐渐成熟,最优模式也可能发生变化。
例如,一个现在可以轻松运行在单台服务器上的系统,未来很可能发展到需要部分组件运行在独立服务器上。在单服务器阶段,源码级解耦或许就足够了;但之后,可能需要升级为可部署单元级别的解耦,甚至服务级别的解耦。
一种目前很流行的方案是默认直接采用服务级解耦。但这种方式的问题在于成本高,并且容易导致粗粒度解耦。无论微服务多么 “微”,其解耦粒度往往仍然不够精细。
服务级解耦的另一个问题是昂贵—— 无论是开发时间,还是系统资源。在不需要服务边界的地方强行划分,是对人力、内存和 CPU 周期的浪费。当然,硬件很便宜,但人力成本绝不便宜。
我个人的偏好是:把解耦做到 “未来可以随时拆成服务” 的程度,但在必要之前,尽可能让组件保留在同一个地址空间中。这样就保留了升级为服务的选择权。
按照这种方式,组件最初在源码级别分离。这可能在项目整个生命周期内都足够使用。如果之后出现开发或部署问题,再将部分解耦提升到部署级别,通常也能支撑一段时间。
随着开发、部署和运行压力持续增加,我会谨慎选择哪些可部署单元需要升级为服务,并逐步引导系统向这个方向演进。
随着时间推移,系统的运行需求也可能下降。曾经需要服务级别解耦的部分,未来可能只需要部署级别,甚至源码级别解耦就够了。
好的架构,应该允许一个系统:
以单体形态诞生,打包成单个文件部署;
随后逐步演进为一组可独立部署的单元;
再进一步成长为独立服务 / 微服务;
而当形势变化时,又能反向回退,重新收缩回单体。
优秀的架构会保护大部分源码不受这些结构变化的影响。它将解耦模式作为一个可选项保留,让大规模部署可以使用一种模式,小规模部署则使用另一种模式。

Conclusion

系统的解耦模式本身,就是一种很可能随时间而变化的东西

posted @ 2026-03-20 13:50  cyusouyiku  阅读(5)  评论(0)    收藏  举报