第二十五章 层级和边界

很容易将系统看作由三个组件组成:UI,业务规则和数据库。对于一些简单的系统来说,这就足够了。但是对于大多数系统来说,组件的数量要比这个多。

例如,考虑一个简单的电脑游戏。很容易想象这三个组成部分。UI处理从玩家到游戏规则的所有消息。游戏规则以某种持久的数据结构存储游戏的状态。但是,这是所有的?

寻找WUMPUS

让我们充实这个骨架。让我们假设这款游戏是从1972年开始的古老的追捕冒险游戏。这款基于文本的游戏使用了非常简单的命令,如GO EAST和SHOOT WEST。玩家输入命令,计算机回应玩家看到的,闻到的,听到的和体验到的东西。玩家在洞穴系统中寻找一个Wumpus,必须避免陷阱,坑等危险。如果你有兴趣,游戏规则很容易在网上找到。

假设我们将保留基于文本的UI,但将其与游戏规则分离,以便我们的版本可以在不同的市场中使用不同的语言。游戏规则将使用与语言无关的API与UI组件进行通信,UI将会将API转换为适当的人类语言。

如果源代码依赖关系管理得当,如图25.1所示,那么任意数量的UI组件都可以重用相同的游戏规则。游戏规则不知道,也不关心正在使用的人类语言。

图25.1任何数量的UI组件都可以重用游戏规则

我们还假设游戏的状态是保存在某个持久性存储上——也许只是在闪存中,或者是在云端,或者是在RAM中。在这些情况下,我们不希望游戏规则知道细节。因此,我们再次创建一个API,游戏规则可以使用该API与数据存储组件进行通信。我们不希望游戏规则知道任何有关不同类型的数据存储的情况,所以依赖关系必须遵循依赖规则进行指向,如图25.2所示。

图25.2 遵循的依赖关系

整洁的架构

应该清楚的是,我们可以很容易地将整洁的架构方法应用于这种情况下,包括所有用例,边界,实体和相应的数据结构。但是我们是否真的找到了所有重要的架构界限?

例如,语言不是UI变化的唯一因素。我们也可能想要改变我们传达文本的机制。例如,我们可能想要使用一个正常的shell窗口,文本消息或聊天应用程序。有许多不同的可能性。

这意味着这个变化因素有一个潜在的架构边界。也许我们应该构建一个跨越这个边界的API,把语言与通信机制隔离开来;这个想法如图25.3所示。

图25.3 修改后的图

图25.3中的图变得有些复杂,但是没有什么新东西。虚线轮廓表示定义由其上方或下方的组件实现的API的抽象组件。例如,Language API是由English和Spanish实现的。

GameRules通过GameRules定义的API和Language实现的API与Language进行通信。Language使用Language定义但TextDelivery实现的API与TextDelivery进行通信。API由使用者定义和拥有,而不是由实现者来定义和拥有。

如果我们要查看GameRules内部,我们会发现GameRules中代码使用的多态边界接口,并由Language组件内部的代码实现。我们还会发现由Language使用的多态边界接口,并由GameRules中的代码实现。

如果我们要查看Language内部,我们会发现同样的东西:由TextDelivery中的代码实现的多态边界接口,以及由TextDelivery使用并由Language实现的多态边界接口。

在每种情况下,由这些边界接口定义的API都归上游组件所有。

诸如英语,SMS和CloudData等变体由抽象API组件中定义的多态接口提供,并由服务于它们的具体组件实现。例如,我们希望语言中定义的多态接口可以用英语和西班牙语来实现。

我们可以通过消除所有的变化,只关注API组件来简化这个图表。图25.4显示了这个图。

图25.4简化图

请注意,该图的方向如图25.4所示,以便所有箭头都指向上方。这将GameRules置于顶端。这个方向是有道理的,因为GameRules是包含最高层策略的组件。

考虑信息流的方向。所有的输入来自用户,通过左下方的TextDelivery组件。该信息通过语言组件上升,并被转换成GameRules命令。GameRules处理用户输入,并将适当的数据发送到右下角的DataStorage。

然后GameRules将输出发送回语言,该语言将API翻译成适当的语言,然后通过TextDelivery将该语言传递给用户。

该组织有效地将数据流划分为两个流。左边的流是关于与用户通信的,而右边的流是关于数据持久性的。GameRules中的两个数据流在前顶部相遇,这是通过两个数据流的数据的最终处理器。

跨越流

这个例子中总是有两个数据流吗?不会的。想象一下,我们想在网络上和多个玩家一起打Hunting the Wumpus。在这种情况下,我们需要一个网络组件,如图25.5所示。这个组织把数据流分成三个流,全部由GameRules控制。

图25.5添加网络组件

所以,随着系统变得越来越复杂,组件结构可能会分裂成许多这样的流。

分割流

现在,你可能会认为所有的流最终都会在单个组件中相遇。如果生活如此简单就好了!当然,现实要复杂得多。

考虑GameRules组件来对于游戏的意义。部分游戏规则处理地图的机制。他们知道这些洞穴是如何连接的,以及哪个物体位于每个洞穴中。他们知道如何将玩家从洞穴移动到另一个洞穴,以及如何确定玩家必须处理的事件。

但是还有一套更高层次的策略,知道玩家生命值,以及特定事件的成本或收益。这些策略可能会导致玩家逐渐失去生命值,或者通过发现食物来获得生命值。较低层的机制策略将向此更高层的策略声明事件,如FoundFood或FellInPit。然后更高一级的策略将管理玩家的状态(如图25.6所示)。最终这个政策会决定玩家是赢还是输。

图25.6 更高层的策略管理玩家

这是一个架构边界吗?我们是否需要一个将MoveManagement与PlayerManagement分开的API?那么,让我们做些更有趣一些的事,并添加微服务。

假设我们已经有了一个大型的多人游戏版本寻找Wumpus。MoveManagement在玩家的计算机内本地处理,但是PlayerManagement由服务器处理。PlayerManagement为所有连接的MoveManagement组件提供微服务API。

图25.7中的图表以一种稍微缩略的方式描述了这种情况。网络元素比描述的要复杂一点,但是你可能仍然可以得到这个想法。在这种情况下,MoveManagement和PlayerManagement之间存在一个完整的架构边界。

图25.7添加一个微服务API

小结

这是什么意思呢?为什么我把这个荒谬的简单的程序,可以在200行Kornshell中实现,并将其与所有这些疯狂的架构边界推断出来?

这个例子是为了表明,架构边界无处不在。作为架构师,我们必须小心地识别何时需要它们。我们也必须意识到,这些边界如果得到充分实施,代价很高。同时,我们必须认识到,当忽略这样的边界时,即使存在全面的测试套件和重构规则,它们在后来的添加也是非常昂贵的。

那么我们做什么,我们的架构师呢?答案是不满意的。一方面,一些非常聪明的人士多年来告诉我们,我们不应该预料抽象的必要性。这是YAGNI的哲学:“你不会需要它”(You aren’t going to need it.)。这个信息是有智慧的,因为过度工程设计通常比缺乏工程设计更糟。另一方面,当你发现你确实需要一个没有的架构边界时,增加这样一个边界的成本和风险就会非常高。

所以你得学会它。亲爱得软件架构师,你必须看到未来。你必须聪明地猜测。你必须权衡成本,确定架构边界在哪里,哪些应该完全实现,哪些应该局部实现,哪些应该被忽视。

但这不是一次性的决定。在项目的开始阶段,你不要简单地决定实现哪个边界,哪个边界要忽视。相反,你看。随着系统的发展,你会注意到。你注意到可能需要边界的地方,然后仔细观察摩擦的第一个注意点,因为这些边界不存在。

在这一点上,你要衡量实现这些边界的成本与忽视它们的成本,并且你经常审查这个决定。你的目标是在实现成本低于忽略成本的拐点实现边界。这需要注意。

万万切记。

results matching ""

    No results matching ""