【51CTO.com快译】不知道您是否听说过“软件架构师最讨厌意大利面”这个梗?它是指软件架构师在设计应用系统时,应当在匹配业务概念的基础上,开发出清晰的架构与流程,避免出现各种在逻辑上相互缠绕,模块与层面关系定义不清,各个功能彼此交织,进而形成难以被运维的“意面式架构(spaghetti architecture)”。下面,我将总结一些值得您遵循的应用架构优秀实践,以便您构建出结构化的、可扩展的应用架构。
为什么应用架构如此重要?
通常,应用架构包含了所有的软件模块、组件、内/外部系统、以及构成应用之间的交互关系。显然,结构良好的应用架构,可以确保您的应用能够根据业务和用户的需求进行预期的扩展。同时,好的架构既能够合理地隔离不同的功能概念,又可以在内/外部形成良好的依赖关系。
相反,如下图所示,如果您在针对各种需求的初期设计时,以及后期的变更中,忽略了对于应用架构的合理构建与维护,那么将会导致不同组件之间,依赖关系的错综复杂,甚至难以同步与管理。
那么在实际项目中,意面式的架构到底会给我们的系统带来哪些危害呢?
- 服务抽象性差:如果未能围绕着核心业务概念,实现正确地隔离和抽象,那么服务就会在不同系统之间分散各种业务规则,进而降低代码的结构化和可重用的程度。
- 难以管理的依赖关系:当组件之间无法被恰当地隔离时,任何针对系统的修改或替换操作,都会产生滚雪球式的效应。也就是说,某一部分的更改,会影响到与之相关联的所有依赖关系。
- 僵化且运行缓慢的旧系统:如果系统本身就很复杂且不灵活,那么我们就需要花费更长的时间,通过调整来适应新的业务变化。而且,如果所用到的技术已经过时,那么随着时间的推移,核心数据与系统会对过时的技术,累积深度的依赖性,从而导致技术更新变得难上加难。
如何构建可扩展的应用架构?
为了构建可靠且可扩展的应用架构,您需要基于严格的定义原则和完善的设计概念。显然,我们的目标是:既需要支持快速的业务增长和大规模的扩容需求,又需要降低部署的难度并避免高昂的代码维护成本。因此,我们可以从如下方面考虑应用架构的设计:
- 在所有项目参与者之间达成共识。
- 支持定义和计划。
- 持续进行变更。
- 管理好系统的复杂性。
- 管控与降低风险。
- 最大限度地减少技术债务(这是任何前瞻性应用架构的最终目标)。
在此,我们引入一个架构画布(Architecture Canvas)概念。作为一个支持和加速架构设计的多层框架,它可以促进对可重用的服务和组件进行抽象。通过保留相对独立的生命周期,架构画布可以最大程度地减少变更所带来的影响,进而使得应用架构更易于维护和扩展。
架构画布的逻辑组成如上图所示。其中,从下往上分别是:
- 基础层:在该层中,您可以实现所有可重用的非功能性需求。例如:连接到外部系统,或者使用可重用的UI模式与主题库,来扩展现有的框架服务。
- 核心层:在该层中,您可以实现各种核心的业务服务。例如:各种围绕着业务概念的服务、业务规则、业务实体、业务交易和业务部件等。您需要让这些服务独立于目标系统,并根据基础服务来抽象出任何可能的整合信息。可见,通过基础层和核心层,您已经隔离出了所有可重用的服务或组件。
- 最终用户层:在该层中,您可以通过使用基础与核心层的服务,来支持用户界面,以及与用户交互的流程。值得注意的是:为了确保整个生命周期的独立性,处于该层面上的模块,不应为其他模块提供服务。
架构的验证
为确保设计架构的合理性,且不会产生“意面式”的烂尾,下面我将为您提供一些可以遵循的准则和建议。
1.不要带有横跨三个层面的向上引用
鉴于前文提到的结构化分层,我们显然不应该让与业务无关的基础服务,去依赖核心业务;也不应该让可重用的服务,依赖各种最终用户的接口。此外,向上引用往往会产生一个群集。如下图所示,在该群集中,存在直接或间接链接关系的任何两个模块,都具有循环依赖性。
在上图中,由于模块B可以间接地影响模块A,而模块A也可以间接地影响模块B,因此,这就是一组相互依赖的模块。此外,如果您有另一个正在使用核心服务B的最终用户模块(EU2),那么它就会依赖整个群集。可见,它们在运行时,不仅会占用大量不必要的资源,还会受到集群中某些模块变化的间接影响。
2.避免最终用户之间的旁路引用
为了确保正确的隔离,并避免最终用户具有不同的生命周期,最终用户模块不应提供可重用的服务。下图展示了最终用户之间的旁路引用关系。
也就是说,如果最终EU1调用到了EU2,则表明EU1无法独立于EU2,同时他也就不能独立于EU2下面的层级结构中的集群。
3.避免在核心模块和基础模块之间进行循环引用
如果您能够遵循前面提到的两个规则,那么就不必担心最终用户模块之间可能出现循环引用。反而,我们应当重点避免在核心模块和基础模块之间,可能出现的循环引用。此类模块之间的循环引用主要产生于:一些业务概念没能被正确地抽象,进而对代码的管理产生不良的影响。
如上图所示,循环引用多发生在如下两种情况中:
- A和B之间的连接相当紧密,甚至它们隶属于同一模块(例如,某个订单或订单项)。
- 根据两个概念之间的既定关系,如果改变一个模块的逻辑位置,其单方面的依赖关系就会被破坏。例如,合同是由客户产生的,但是客户的存在则无需合同的引用。
4.额外的建议
- 核心模块不应具有前端的筛选条件:如果要实现某个服务,您可能需要添加一些筛选条件,用以进行单元测试。但是作为开发人员,一旦完成了代码测试,就应该及时去除掉测试的筛选条件。如果出于某种原因,仍需要使用测试筛选条件,来支持某些回归测试或BDD(行为驱动开发)测试的话,您就需要将其移至最终用户的测试模块中。毕竟,将测试筛选条件保留在核心模块上,是非常危险的。基于风险管控的考虑,它们只能存在测试环境中,而不能留在生产环境里。
- 所有实体都应当被发布为只读:通过该实践,您可以禁止访问者(consumers)简单粗暴地在数据库中创建、更新或删除记录。在核心服务层面上,您应当抽象出业务事务、验证、规范化、以及审核等需要与其他系统集成的组件。在实际项目中,正确的做法是:将所有的业务交易的实施,都发布给使用者,同时提供安全且恰当的抽象服务。
- 避免在基础层面上使用业务逻辑:有时候,人们会倾向于在该层面上实现各种业务规则。但实际上,我们应当确保它与业务无关,并能在任何应用领域中被重用。
- 不要在基础层面上添加核心业务实体:为了与业务无关,基础模块不应具有与业务相关的实体。不过,它们可以通过带有非业务的实体,以支持应用的某些非功能性需求。例如:如果您需要创建通用的服务,来审计所有事务,那么就可以创建一个审计实体。毕竟,某个软件应用的主要业务可能并非审计,而是销售产品,拉新客户或变更合同等。
使用架构画布的应用组合
在讨论应用组合之前,我先声明一下:这里所说的“应用”,与我们通常在业务环境中所提及的“应用”,具有不同的含义。在该语境中,我们使用术语“应用”来指代 在开发环境中的最小部署单元。它既可以是被用于管理的所有环境,也可以是业务应用、IT用户、安全性集合、以及应用单个模块等。
为了识别应用到底属于上面提到的哪个层级,您应该对目标应用进行深入分析和模块化的解构。例如:如果某个应用将带有最终用户模块,那么它肯定属于最终用户层面。
下面是一组能够确保设计出前瞻架构的参考规则:
规则1:从模块的架构画布准则开始
我们应按照上面给出的建议,对模块进行正确地分层。
规则2:隔离公共服务
将各个模块正确地放置到位后,我们就可以开始设计应用了。如前所述,如果在“最终用户应用2”上有一个模块会使用到“最终用户应用1”上某一个模块,那么我们就应该对通用核心应用进行隔离,以免产生依赖性。如下图所示,如果两个应用要进行内容上的共享与交互,则需要在彼此隔离的情况下,通过通用的应用服务来实现。
规则3:请勿混淆所有者角色
如果一个应用拥有多个所有者,那么由于责任不清,可能会导致变更的内容与管理过于复杂。我们可以通过所有权的聚合与分拆(如下图)两个方式,给每个应用明确设定一个所有者。
规则4:分清参与者角色
与所有者角色类似,参与者也有各自不同的节奏。例如:有一个提供不同保险业务的门户网站。其所有业务线都处于同一个应用中,那么任何一个业务(例如车险业务)的任何更改都不可能独立于其他业务。因此,实际上是由那个最慢的业务线,决定了整体应用的发布周期。
如下图所示,我们需要通过在每条业务线中创建单独的应用,让每个参与者都可以预估自己的交付速度。在此基础上,我们可以根据项目的具体需求,或是将不同参与者的任务相互隔离,或是通过内容共享的方式,加强他们的协作。
原文标题:Application Architecture: Best Practices for Future-Proofing Your Apps,作者: Francisco Menezes
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】