构建完整的架构边界的成本是很高的。需要为系统设计双向多态的边界接口、输入和输出数据结构以及所有必要的依赖管理,以便将两个部分隔离为可独立编译和部署的组件。这涉及大量的工作和后期维护。
许多情况下,一个好的架构师可能会认为设置这样的边界的开支太高,但仍然希望为可能需要的边界留下位置。
这种预判性设计常常被敏捷社区中的许多人指责为违反 YAGNI(你不会需要它)原则。然而,架构师可能会看到问题并想到:“是啊,但是我可能会需要’在这种情况下,他们可能会实现一个不完全边界。
构造不完全边界的一种方法是 将系统分割成一系列可以独立编译和部署的组件,之后再把它们组合成一个组件。 换言之,将相应的接口和输入/输出数据结构都设置好,并且一切都已准备妥当,但我们仍选择将这些组件编译和部署为单个组件。
显然,这种不完全边界需要与完全边界同样数量的代码和设计工作。但是它不需要管理多个组件。没有版本号跟踪或发布管理负担。这一差异不容忽视。这是 FitNesse 的早期策略。FitNesse 的网络服务器组件被设计为可与 FitNesse的 wiki 和测试部分分离。这样,我们就可以使用该网络组件创建其他基于 Web 的应用程序。同时,我们不希望用户下载两个组件。请记住,我们的一个设计目标是“下载并运行”。我们的意图是用户下载一个jar 文件并执行它,而不必寻找其他 jar 文件,解决版本兼容性等问题。
FitNesse 的经历也揭示了这种方式的其中一个危害。随着时间的推移,人们逐渐认识到不再需要单独的 Web 组件,Web 组件和 wiki 组件之间的分离开始变得脆弱。依赖关系开始朝着错误的方向交叉。现在,重新分离它们可能会是一项烦琐的任务。
完全成熟的架构边界使用双向边界接口来保持双向隔离。维护这种双向的隔离性,通常不会是一次性的工作,需要我们持续投入资源。
图 24.1 显示了一个更简单的结构,用于暂时保留扩展为完全边界的位置它展示了传统的策略模式。serviceBoundary(服务边界)接口由客户端使用并由 serviceImpl(服务具体实现)类实现。
上述设计显然为未来的架构边界打下了基础。为了将 Client(客户端)与ServiceImpl隔离开来,必须进行必要的依赖反转。同时,我们清楚看到分离可能会相当迅速地降级,如图示中的恶意虚线所示。由于没有双向接口,除了依赖开发人员和架构师的勤勉和自律,没有任何事物可以防止这种反向通道的出现。
一个更简单的边界是外观模式,如图24.2所示。在这种情况下,不需要使用依赖反转。该边界仅由 Facade(外观)类定义,该类将所有服务列为方法,并将服务调用部署到客户端不应访问的类中。
请注意,客户端对所有这些服务类都具有传递性依赖。在静态语言中,对其中一个服务类源代码的更改将强制客户端重新编译。此外,你可以想象使用此结构创建反向通道是多么容易。
我们已经看到了部分实现不完全边界的三种简单方法。当然,还有很多其他方法。这三种策略仅作为示例提供。
这些方法中的每一种都有其自己的成本和收益。在特定的情况下,每种方法都是适宜的,作为一个最终完全边界的替代方案。如果该边界从未实现,则每种方法都可能会受到损害。