《架构整洁之道》第 25 章 层次与边界

2023-06-13 09:56:58 浏览数 (3)

人们习惯将系统分为三个组件:UI业务逻辑和数据库。对于一些简单的系统来说,三个就够了,但是稍微复杂一点的系统组件就不止这三个了。

以一个简单的游戏为例,粗看似乎也符合三个组件的架构设定。

首先UI接收用户输入数据,然后将数据传输给业务逻辑,最后数据入库。但仅仅只是这样吗?

基于文字的冒险游戏:Hunt The Wumpus

文字游戏,输入一些命令,游戏会返回对应的场景和执行动作。

现在决定包留这种基于文本的UI,但是要将UI和游戏业务逻辑之间的耦合解开,以便在不同地区使用不同的语言。

也就是说游戏业务逻辑UI的交互,不会使用自然语言,UI会将游戏业务逻辑传回的数据,转换成对应的自然语言。这就能做到多套UI可以复用同一个业务逻辑,而游戏的业务逻辑组件也不需要知道UI使用的是哪个语言。

业务逻辑(Game Rules),处理完UI给的数据后,就需要将数据存储了,但是我们不希望它只依赖某一种存储介质,所以让其存储组件依赖业务逻辑组件,遵守依赖关系原则。

可否采用整洁架构

这里我们只将它划分出来了组件,但是还没有找到所有的架构边界。

比如,语言并不是UI唯一的变动理由,因为我们还可能改变输入方式,例如,短信,Web,命令行,或者聊天程序。

这就意味着这类输入方式的变更,也应该需要一个对应的架构边界,需要构造一个API,以便将语言部分和输入方式部分隔离开。

这里的虚线,代表的是抽象组件(Boundary多态接口),具体实现类都是实线框。它们定义的API通常需要上下游组件来实现。

如果我们查看内部源码,会发现如下依赖关系。

比如Language组件的部分API是由EnglishSpanish组件来实现的。GameRules组件的部分API是由Language组件实现的。即,这些API的定义和维护是由使用方来定义和维护的,而非实现方。(被依赖被调用方只定义,调用方使用方负责实现和通信内容)。你想传什么给我,由你说。

TextDelivery使用的Boundary多态接口,由Language来定义实现,Language使用的Boundary多态接口,也有由TextDelivery来定义实现。

在这所有场景中,由Boundary接口所定义的API都是由使用者的上一层组件负责维护的。

如果我们去掉具体的实现类,只保留接口组件依赖结构进行简化,可以得到下面这张组件依赖图。

可以看到,所有的依赖都是向上的,很好的反映了GameRules作为最高策略组件的事实。

信息流方向:

来自用户的所有信息,都会通过TextDelievery组件传入。当信息流转到Language组件时,就会转换为具体的命令输入给GameRules组件,之后GameRules组件会将数据发送个DataStorage组件,接下来GameRules会将输出传递到Language组件,Language组件转换为合适的语言并通过TextDelievery将语言传递给用户。

在虚线左边的数据流关注用户的通信,右侧数据流关注数据的持久化。两边的数据流在顶部的GameRules汇聚,它是所有数据流的最终处理者。

交汇数据流

那么是不是意味着永远只有这两条数据流呢?当然不是的,如果这个游戏变成联机游戏,将会有三条。由此可见,随着系统的进化,组件在架构中自然会分裂出多条数据流来。

数据流的分隔

但在现实中,不会所有的数据流都最终会汇聚到一个组件上。

Hunt The Wumpus这个游戏中,有部分业务逻辑是处理玩家在地图中的行走,GameRules组件需要知道游戏中的洞穴如何相连,每个洞穴都存在什么物品,如何将玩家从一个洞穴转移到另一个洞穴,如何触发各类游戏事件等。

但是在游戏中,还有一个更高层次的策略,这个策略负责了解玩家的血量以及每个事件的后果和影响。这些策略会让玩家掉血或者加血。低层次的策略,负责向高层次的策略传递事件,例如FoundFoodFellInPit。高层次策略则要管理玩家的状态,最终该策略会决定玩家在游戏中的输赢。

以上是否属于架构边界呢?是否需要设计一个API来分隔MoveManagementPlayerManagement呢?回答这些问题前,我们可以把问题弄得更有意思点,加上微服务。

假定这个游戏面向海量用户,MoveManagement组件是运行在用户的本地计算机上,而PlayerManagement则部署在服务端处理,并为MoveManagement提供一个微服务API

在下图中,为该游戏绘制一个简化版的设计图。图中可以看到MoveManagementPlayerManagement有一个完整的系统架构边界。

本章小结

这个游戏很小只有几百行代码。举这个例子的目的在于证明架构边界可以存在任何地方。作为架构师我们必须小心审视什么地方才需要设计架构边界,另外还必须知道这个边界将会带来多大的成本。

作为架构师,我们应该怎么办?这个问题恐怕没有答案。因为一些很聪明的人一直在呼吁,不应该将未来的需求抽象化,YAGNI,You aren't going to need it,臆想中的需求事实上往往不存在,因为过度设计往往比设计不足还糟糕。但另一方面,如果我们需要设置边界,而没有设置边界,等到后面再去添加边界时,成本和风险往往会很高。

现实就是这样,我们必须要有一点未卜先知的能力,有时候要用脑子去猜。软件架构师必须要权衡成本和风险,决定哪里需要设计边界,哪些是完整边界,哪些是不完全边界,还有哪些是可以忽略的。

并且这不是一次性的工作,架构师必须持续观察系统的演进,时刻注意边界设计。然后权衡设置边界成本与不设置的成本。当设置边界的优势超过了不设置时,就是设置边界的最佳时期。

持之以恒,一刻也不能放松。

1 人点赞