第十六章 独立性
讲到了这里,一个好的架构必须能支持:
- 系统的运行和用例
- 系统维护
- 系统开发
- 系统部署
用例
来讨论第一个,用例意味着系统的体系结构必须支持系统的意图。如果系统是购物车应用程序,那么架构必须支持购物车用例。事实上,这是架构师首先关心的问题,也是架构的首要任务。架构必须支持用例。
但是,正如我们之前所讨论的,架构对系统的行为没有太大的影响。架构可以开放的行为选项很少。但影响力不是一切。一个好的架构可以做的最重要的事情是支持行为,是阐明和揭示这个行为,以便系统的意图在架构层面上是可见的。
具有良好架构的购物车应用程序看起来就有购物车应用程序的样子。该系统的用例将在该系统的结构中清楚可见。开发人员不必寻找行为,因为这些行为将成为系统顶层可见的顶层元素。这些元素将是架构中具有显着位置的类或函数或模块,并且将具有清楚地描述其功能的名称。
第二十一章“架构的呐喊”将使这一点更为清晰。
运行
架构在支持系统运行方面扮演着更重要的角色,而不是修缮的角色。如果系统必须每秒处理100,000个用户,那么架构必须支持这种吞吐量和响应时间,以满足每个需求的用例。如果系统必须在毫秒级查询庞大多维的数据集,那么架构的构造中必须考虑可支撑这种操作。
对于某些系统,这意味着将系统的处理元素排列成一个小型服务的阵列,可以在许多不同的服务器上并行运行。对于其他系统,这意味着在单个处理器内共享单个进程的地址空间的过多的轻量级线程。还有一些其他的系统只需要在隔离的地址空间中运行几个进程。有些系统甚至可以作为简单的单一程序运行在单个进程中生存。
看起来很奇怪,但这个决定是一个好的架构师选择许多开放选项中的一个。作为一个整体编写的系统,取决于这个整体结构,如果需要的话,不能很容易地升级到多进程,多线程或者微服务。相比之下,当系统的运行需求发生变化时,保持其组件的适当隔离并且不承担这些组件之间的通信手段的架构随着时间的推移将更容易地通过线程,进程和服务的范围来转换。
开发
架构在支持开发环境方面起着重要的作用。这就是康威法则的起源。康威法则说:
设计一个系统的任何组织都将产生一个结构是该组织通信结构副本的设计。
一个有许多团队和许多关注点的组织必须开发一个系统,这个系统必须有一个架构,以促进这些团队的独立行动,这样团队在开发过程中就不会互相干扰。这是通过将系统合理分区成完全隔离的,可独立开发的组件来实现的。这些组件可以分配给可以独立工作的团队。
部署
架构在决定系统部署的便易性方面也起着巨大的作用。目标是“立即部署”。一个好的架构不该依赖于几十个小的配置脚本和属性文件的调整。它不需要手动创建和安排必要的目录或文件。良好的体系结构有助于系统在构建后立即部署。
再次,这是通过对系统组件进行适当的分区和隔离来实现的,包括将整个系统连接在一起的主要组件,并确保每个组件都正确启动,集成和监控。
保持选项开放
一个好的架构可以平衡所有这些问题,构造相互满足的组件结构。听起来很简单,对吧?好吧,只是写起来很容易。
现实是要达到这个平衡是非常困难的。问题是大多数时候我们不知道所有的用例是什么,也不知道运行约束,团队结构或部署需求。更糟糕的是,即使我们已经了解了这些情况,随着系统在其生命周期中的演化,它们也将不可避免地发生变化。总之,我们必须达到的目标是模糊的,易变的。这就是真实的情况。
但是一切没这么糟:一些架构原则的实施起来相对成本低,并且可以帮助平衡这些问题,即使你没有一个必须达成的清晰的目标。这些原则帮助我们将系统划分为隔离良好的组件,从而尽可能多地保留尽可能多的选项。
良好的体系结构使得系统易于改变,通过开放多的选项,这种改变可以在所有方式进行。
层级解耦
考虑用例。架构师希望系统的结构支持所有必要的用例,但不知道所有这些用例是什么。但是,架构师真正知道系统的基本意图。比如一个购物车系统,或者是物料清单系统,或者是一个订单处理系统。因此,架构师可以通过考虑系统意图,采用单一职责原则(SRP)和共同封闭原则(CCP)来分离那些因不同原因而变化的事物,并且集合那些因同样原因而变化的事物。
那些是不同原因造成的改变?有一些明显的例子。用户界面(UI)可以因为和业务规则无关的原因而改变。用例包含两者的元素。显然,一个好的架构师会希望将用例的用户界面部分与业务规则部分分离开来,以便可以彼此独立地进行更改,同时保持这些用例的可见性和清晰性。
业务规则本身可能与应用程序紧密相关,也可能更通用。例如,输入字段的验证是与应用程序本身紧密相关的业务规则。相反,帐户利息计算与存货盘点则是与该领域更密切相关的业务规则。这两种不同的规则的变化速度不同,并且由不同的原因造成的,所以它们应该是分开的,以便它们能够被独立地改变。
数据库,查询语言甚至模式都是与业务规则或UI无关的技术细节。它们的变化速度不同,并且由不同的原因造成的,因此应与系统的其他方面无关。因此,架构应该将它们与系统的其他部分分开,以便可以独立地改变它们。
因此,我们发现系统分为解耦的平行层——UI和特定于应用程序的业务规则,独立于应用程序的业务规则以及数据库。
用例解耦
还有什么其他的不同原因造成的变化?用例本身!用于向订单输入系统添加订单的用例和从系统删除订单的用例,几乎肯定会以不同的速率改变,并且由于不同的原因。用例是划分系统的一种非常自然的方式。
同时,用例是切割系统水平层的狭窄垂直切片。每个用例都使用一些UI,一些特定于应用程序的业务规则,一些与应用程序无关的业务规则以及一些数据库功能。因此,当我们将系统划分为平行层时,我们也将系统划分为贯穿这些层的垂直用例。
为了实现这种解耦,我们将添加订单用例的用户界面与删除订单用例的用户界面分开。我们对业务规则和数据库也是这样做的。我们把用例分解成系统的垂直高度。
你可以看到这里的模式(pattern)。如果你将因不同原因而更改的系统元素分开,则可以继续添加新的用例而不会干扰旧的用例。如果你还将用户界面和数据库分组以支持这些用例,以便每个用例都使用UI和数据库的不同方面,那么添加新的用例将不太可能影响到较旧的用例。
模式解耦(DECOUPLING MODE)
现在想想对于运行,解耦的意义是什么。如果用例的不同方面是分开的,那么那些必须以高吞吐量运行的应用程序可能已经与那些必须以低吞吐量运行的应用程序分开。如果UI和数据库已经与业务规则分离,那么它们可以运行在不同的服务器上。那些需要更高带宽的可以复制到许多服务器上。
简而言之,我们为了用例而做的解耦也有助于运行。但是,为了利用运行效益,解耦必须有适当的模式。要在不同的服务器上运行,分离的组件不能依赖于处理器的相同地址空间。它们必须是独立的服务,通过某种网络进行通信。
许多架构师称这些组件为“服务”或“微服务”,这取决于一些模糊的行数。事实上,基于服务的体系结构通常被称为面向服务的架构(service-oriented architecture)。
如果这个命名法在你的脑海里掀起了一些警钟,别担心。我不打算告诉你,SoA是最好的架构,或者微型服务是未来的潮流。这里要说的是,有时候我们必须把我们的组件分解到服务级别。
请记住,一个好的架构可以让选项开放。解耦模式是这些选项之一。
在我们进一步探讨这个话题之前,让我们来看看另外两个。
独立的开发能力
第三个是开发。很明显,当组件被强力分离时,团队之间的干扰得到缓解。如果业务规则不了解用户界面,那么专注于用户界面的团队不会对注重业务规则的团队产生太大的影响。如果用例本身彼此分离,那么专注于addOrder用例的团队不太可能干扰关注deleteOrder用例的团队。
只要层与用例分离,系统的体系结构将支持团队的组织,而不管它们是组织为特性团队,组件团队,层组还是其他变体。
独立的可部署性
用例和层的分离也为部署提供了高度的灵活性。事实上,如果解耦合完成,那么应该可以在运行系统中热交换层和用例。添加一个新的用例可能很简单,只需向系统中添加一些新的jar文件或服务而保持系统其它部分不变。
冗余
架构师往往陷入陷阱——一个取决于他们担心冗余的陷阱。
冗余通常是软件中的一件坏事。我们不喜欢冗余的代码。当代码真正冗余的时候,我们有作为专业人士的觉悟,去减少和消除冗余。
但是有不同种类的冗余。有真实的冗余,其中一个实例的每个变化都需要对该实例的每个副本进行相同的更改。也有虚假或者意外的冗余。如果两个明显冗余的代码段沿着不同的路径发展,如果它们以不同的速率改变并且由于不同的原因,那么它们不是真实的冗余。几年后再看它们,你会发现它们彼此非常不同。
现在想象两个屏幕结构非常相似的用例。架构师可能会强烈地分享这个结构的代码。但他们是这样吗?这是真的冗余吗?或者是偶然的?
这很可能是偶然的。随着时间的推移,这两个屏幕将会发生分歧,并最终看起来非常不同。出于这个原因,必须小心避免统一它们。否则,将它们分开将是一个挑战。
当你垂直分隔用例时,你会遇到这个问题,你的诱惑将是耦合用例,因为它们具有类似的屏幕结构,类似的算法或类似的数据库查询and或or模式。小心。抵制消除冗余的罪恶感的诱惑。确保冗余是真实的。
同样的道理,在水平分离图层时,可能会注意到特定数据库记录的数据结构与特定屏幕视图的数据结构非常相似。您可能会试图简单地将数据库记录传递给UI,而不是创建看起来相同的视图模型,然后复制这些元素。小心:这种冗余几乎肯定是偶然的。创建单独的视图模型并不是很费力,它可以帮助你保持图层的正确解耦。
模式解耦(续)
回到模式。有很多方法来分离图层和用例。它们可以在源代码级别,二进制代码(部署)级别和执行单元(服务)级别分离。
源代码级别。我们可以控制源代码模块之间的依赖关系,以便更改一个模块不会强制更改或重新编译其他模块(例如,Ruby Gems)。在这种解耦模式下,组件都在相同的地址空间中执行,并使用简单的函数调用相互通信。有一个单一的可执行文件加载到计算机内存。人们经常称这是一个单一的结构。
部署级别。我们可以控制可部署单元(如jar文件,DLL或共享库)之间的依赖关系,以便对一个模块中源代码的更改不强制重建和重新部署其他部分。许多组件可能仍然位于相同的地址空间,并通过函数调用进行通信。其他组件可能驻留在同一处理器中的其他进程中,并通过进程间通信,套接字或共享内存进行通信。这里最重要的是将分离的组件分区为可独立部署的单元,如jar文件,Gem文件或DLL。
服务级别。我们可以将依赖关系降低到数据结构的水平,并且仅通过网络数据包进行通信,使得每个执行单元完全独立于对其他源(例如服务或微服务)的源代码和二进制变化。
使用什么模式是最好的?
答案是,在项目的早期阶段很难知道哪种模式是最好的。事实上,随着项目的成熟,最佳模式可能会改变。
例如,不难想象,现在在一台服务器上运行的系统可能会增长到某些组件应该在不同服务器上运行的程度。当系统运行在单个服务器上时,源级别的解耦可能就足够了。但是,稍后可能需要将其解耦为可部署单元甚至服务。
一种解决方案(目前看来很流行)是在服务级别上简单地解耦。这种方法的一个问题是它昂贵,并倾向粗粒解耦。无论微服务如何“微”,解耦都不可能精细化。
服务级别解耦的另一个问题是在开发时间和系统资源方面都很昂贵。处理没有任何需要的服务边界是浪费精力,存储和周期。是的,我知道后两个是廉价的,但第一个不是。
我的意愿是推动解耦到可以形成服务的地步。如果有必要的话;但随后尽可能长时间地将组件留在相同的地址空间中。这保持了服务的选项开放。
采用这种方法,最初组件在源代码级分离。在项目的整个生命周期内,这可能是足够好的。但是,如果出现部署或开发问题,将部分解耦到部署级别可能就足够了,至少维持一段时间。
随着开发,部署和运行问题的增多,我们要慎重选择哪些可部署级别转入服务级别,并逐步向这个方向转移。
随着时间的推移,系统的运行需求可能会下降。曾经需要在服务级别解耦的现在可能只需要部署级别甚至源代码级别的解耦。
一个好的体系结构将允许一个系统作为一个庞然大物出现,部署在一个文件中,然后成长为一套可独立部署的单元,然后一直到独立的服务或者还有微服务。后来,随着事情的变化,它应该允许扭转这种进展,并一路退化进整体中。
一个好的架构可以保护大部分源代码不受这些变化的影响。它将解耦模式作为选项开放,以便大型部署可以使用一种模式,而小型部署可以使用另一种模式。
小结
是的,这是种小技巧。我并不是说解耦模式的改变应该是一个简单的配置选项(尽管有时候这是合适的)。我所说的是,一个系统的解耦模式是可能随时间而改变的事情之一,一个好的架构师会预见并恰当地促进这些变化。