人们习惯将系统分为三个组件:UI
,业务逻辑
,和数据库
。对于一些简单的系统来说,三个就够了,但是稍微复杂一点的系统组件就不止这三个了。
以一个简单的游戏为例,粗看似乎也符合三个组件的架构设定。
首先UI
接收用户输入数据,然后将数据传输给业务逻辑,最后数据入库。但仅仅只是这样吗?
基于文字的冒险游戏:Hunt The Wumpus
文字游戏,输入一些命令,游戏会返回对应的场景和执行动作。
现在决定包留这种基于文本的UI
,但是要将UI
和游戏业务逻辑
之间的耦合解开,以便在不同地区使用不同的语言。
也就是说游戏业务逻辑
和UI
的交互,不会使用自然语言,UI
会将游戏业务逻辑
传回的数据,转换成对应的自然语言。这就能做到多套UI
可以复用同一个业务逻辑
,而游戏的业务逻辑组件
也不需要知道UI
使用的是哪个语言。
业务逻辑(Game Rules)
,处理完UI
给的数据后,就需要将数据存储了,但是我们不希望它只依赖某一种存储介质,所以让其存储组件依赖业务逻辑
组件,遵守依赖关系原则。
可否采用整洁架构
这里我们只将它划分出来了组件,但是还没有找到所有的架构边界。
比如,语言并不是UI
唯一的变动理由,因为我们还可能改变输入方式,例如,短信,Web,命令行,或者聊天程序。
这就意味着这类输入方式的变更,也应该需要一个对应的架构边界,需要构造一个API
,以便将语言部分和输入方式部分隔离开。
这里的虚线
,代表的是抽象组件(Boundary多态接口)
,具体实现类都是实线框。它们定义的API
通常需要上下游组件来实现。
如果我们查看内部源码,会发现如下依赖关系。
比如Language
组件的部分API
是由English
和Spanish
组件来实现的。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
组件需要知道游戏中的洞穴如何相连,每个洞穴都存在什么物品,如何将玩家从一个洞穴转移到另一个洞穴,如何触发各类游戏事件等。
但是在游戏中,还有一个更高层次的策略,这个策略负责了解玩家的血量以及每个事件的后果和影响。这些策略会让玩家掉血或者加血。低层次的策略,负责向高层次的策略传递事件,例如FoundFood
和FellInPit
。高层次策略则要管理玩家的状态,最终该策略会决定玩家在游戏中的输赢。
以上是否属于架构边界呢?是否需要设计一个API
来分隔MoveManagement
和PlayerManagement
呢?回答这些问题前,我们可以把问题弄得更有意思点,加上微服务。
假定这个游戏面向海量用户,MoveManagement
组件是运行在用户的本地计算机上,而PlayerManagement
则部署在服务端处理,并为MoveManagement
提供一个微服务API
。
在下图中,为该游戏绘制一个简化版的设计图。图中可以看到MoveManagement
和PlayerManagement
有一个完整的系统架构边界。
本章小结
这个游戏很小只有几百行代码。举这个例子的目的在于证明架构边界可以存在任何地方。作为架构师我们必须小心审视什么地方才需要设计架构边界,另外还必须知道这个边界将会带来多大的成本。
作为架构师,我们应该怎么办?这个问题恐怕没有答案。因为一些很聪明的人一直在呼吁,不应该将未来的需求抽象化,YAGNI,You aren't going to need it
,臆想中的需求事实上往往不存在,因为过度设计往往比设计不足还糟糕。但另一方面,如果我们需要设置边界,而没有设置边界,等到后面再去添加边界时,成本和风险往往会很高。
现实就是这样,我们必须要有一点未卜先知的能力,有时候要用脑子去猜。软件架构师必须要权衡成本和风险,决定哪里需要设计边界,哪些是完整边界,哪些是不完全边界,还有哪些是可以忽略的。
并且这不是一次性的工作,架构师必须持续观察系统的演进,时刻注意边界设计。然后权衡设置边界成本与不设置的成本。当设置边界的优势超过了不设置时,就是设置边界的最佳时期。
持之以恒,一刻也不能放松。