当前位置: 首页 > 科技观察

阿里资深技术专家:一个工整的应用架构“长什么样”?

时间:2023-03-20 23:29:42 科技观察

作者张建飞是阿里巴巴资深技术专家。他在公司工作了6年,创立了COLA。希望能探索出一套实用的应用架构规范。这个规范不是高深的理论,而是一种可以复制、可以理解、可以实施、可以控制复杂度的指导和约束。本文详细介绍了他对COLA的升级迭代。很多同学不止一次的跟我说,我们的系统很乱,主要表现在:应用的层次结构很乱:不知道应用应该怎么分层,应该包含哪些组件,它们之间有什么关系组件是;缺乏规范的指导和约束:不知道在什么地方添加新的业务逻辑(哪个类,哪个包),取什么名字比较合适?为了解决这些问题,我创建了COLA(https://github.com/alibaba/COLA),我们的初衷之一是试图探索一套实用的应用架构规范。这个规范不是高深的理论,而是一种可以复制、可以理解、可以实施、可以控制复杂度的指导和约束。自COLA诞生以来,我收到了很多意见和建议。同时,在我自己的实践过程中,我也发现了COLA1.0的很多不足之处。有些设计是多余的,没有必要,有些关键要素没有包括在内。比如我最近一直在思考的核心应用架构和复杂的业务代码治理,就是对COLA1.0的反思。结合实践中的探索和对复杂性治理的不断思考,我决定对COLA进行全面升级,于是就有了现在的COLA2.0。从1.0到2.0,不仅仅是数字的简单变化,更是架构理念和设计理念的升级。主要变化包括:新架构分层:领域层不再直接依赖于基础设施层。新组件划分:重新定义和划分组件,添加新组件,并移除一些旧组件(Validator、Convertor等)。新的扩展点设计:引入了一个新的概念,使扩展更加灵活。新二方库的定位:二方库不仅仅是DTO,更是DomainModel的轻量级表达和实现。新架构分层在COLA1.0中,我们的分层是下图所示的经典分层结构:在COLA2.0中,这些层还是一样的,只是依赖关系发生了变化。Domain层不再直接依赖于Infrastructure层,而是引入了Gateway的概念,使用DIP(DependencyInversionPrinciple,依赖倒置)来反转Domain层和Infrastructure层之间的依赖关系。关系如下图所示:这样做的好处是Domain层会变得更纯粹,彻底摆脱对技术细节的依赖(以及技术细节带来的复杂性),只需要处理业务逻辑安心。除此之外还有两个好处:1.并行开发:只要约定好Domain和Infrastructure的接口,两个同学就可以并行编写Domain和Infrastructure的代码。2.可测试性:没有任何依赖的领域被POJO类填满,单元测试会变得很方便,也很适合TDD开发。新的组件划分模块和组件的定义首先要明确组件概念的定义。在Java中(或者在本文中),组件的作用域是Java的包。还有一个词叫模块(Module),组件和模块这两个概念比较容易混淆。比如在《实现领域驱动设计》中,作者说:如果你在使用Java或者C#,你已经对Modules很熟悉了,虽然你知道它们的另一个名字。Java称它们为包。C#称它们为命名空间。他认为Module就是Package,我觉得这个定义很混乱。特别是在使用Maven的时候,在Maven中,Module是一个Artifact,通常是Jar而不是Package。例如,COLAFramework包括以下四个模块:确实,Module和Component这两个概念很相似,很容易造成混淆。比如StackOverflow上有一道题[1],就是问Module和Component的区别。得票最多的答案按范围区分。条款是相似的。我通常认为“模块”比“组件”大。组件是单个部分,通常范围相对较小。这个答案和我的直觉反应是一致的,就是Module比Component要大。根据以上信息,我将在这里定义Module和Component。在本文中,将遵循以下定义和符号。模块(Module):与Maven中Module的定义一致,简单理解为Jar。用立方体表示。Component:类似于UML中的定义,简单理解就是Package。用UML组件图表示。一个Moudle通常由多个组件组成,它们之间的关系和表示法如下图所示:COLA2.0的组件在COLA2.0中,我们对组件进行了重新设计,引入了一些新组件,并删除了一些旧组件。这些变化的目的是让应用结构更加清晰,组件职责更加清晰,从而更好地提供开发指导和约束。新的组件结构如下图所示:这些组件中的每一个都有自己的职责范围,组件的职责是COLA的重要组成部分,也就是我们上面所说的“指导和约束”。这些组件的详细职责描述如下:二方库中的组件:api:存放应用程序的对外接口。dto.domainmodel:用于数据传输的轻量级领域对象。dto.domainevent:用于数据传输的领域事件。Application中的组件:service:接口实现的门面,没有业务逻辑,可以包含不同终端的适配器。eventhandler:处理域事件,包括本地域和外域。Executor:用来处理命令(Command)和查询(Query)。对于复杂的业务,可以包括阶段和步骤。拦截器:COLA提供的针对所有请求的AOP处理机制。Domain中的组件:domain:领域实体,允许继承domainmodel。domainservice:领域服务,用于提供更粗粒度的领域能力。gateway:对外依赖的网关接口,包括storage、RPC、Search等。Infrastructure中的组件:config:与配置信息相关。message:与消息处理相关。repository:存储相关,是gateway的一个特化,主要用于本域的数据CRUD操作。gateway:对外依赖的gateway接口(Domain中的gateway)的实现。在使用COLA时,请尽量按照组件规范的约束来构建我们的应用程序。这使我们的应用程序具有清晰的结构和可遵循的规则。这样一来,代码的可维护性和可理解性就会大大提高。新的扩展点设计引入新概念在讨论之前,我们先明确一下COLA2.0扩展设计中引入的新概念:业务、用例和场景。业务:是自负盈亏的金融实体。例如,天猫、淘宝和零售快递是三个不同的业务。UseCase:描述了用户与系统的交互,每个用例都提供了一个或多个场景。例如,支付订单是一个典型的用例。场景:场景也称为用例的实例(Instance),包括用例的所有可能情况(正常和异常)。比如“订单支付”的用例,有“可以用花呗”、“支付宝余额不足”、“银行账户余额不足”等多种场景。简单的说,一个业务是由多个用例组成的,一个用例又是由多个场景组成的。以淘宝为例,业务、用例、场景的关系如下:新扩展点的实现在COLA2.0,扩展实现机制不变。主要变化在于上面介绍的新概念。因为COLA1.0的扩展设计思路来自星环,原来的扩展粒度也抄袭了星环的“业务标识”。COLA1.0的扩展定位方式如下图所示:但在实际工作中,像星链这样能够支持多种服务的场景并不常见。更多的是对不同用例的差异化支持,或者说同一个用例的不同场景。比如“创建一个产品”和“更新一个产品”是两个用例,但是大部分业务代码是可以复用的,只需要区分一小部分。为了支持这种更细粒度的扩展支持,除了之前的“业务标识(BizId)”之外,我还引入了UseCase和Scenario的概??念。新的扩展定位如下图所示:可以看出,在新的扩展框架下,原来只支持“业务标识”的扩展,现在可以支持“业务标识”、“用例”的扩展”和“场景”。层次扩展无疑比以前灵活多了,在表达和理解上也比以前更好。在新的扩展框架下,例如我们实现上图所示的扩展:在天猫业务-订单用例-88VIP场景-扩展用户身份验证,我们只需要声明一个如下扩展即可实施(扩展)很好。新二方库的定位表面上看,二方库的定位是一个简单的问题,因为服务的二方库无非是用来暴露接口和传递数据(DTO)。然而,深入思考,这并不是一个简单的问题,因为它涉及到不同限界上下文(BoundedContext)之间的协作。如何在分布式环境中实现不同服务(SOA、RPC、微服务,名称不同,本质相同)之间的协作,是一个重要的架构设计问题。限界上下文之间的协作如何实现不同领域之间的协作,同时保证各自领域概念的完整性,有一套方法论。一般来说,大概有两种方式:共享内核(SharedKernel)和反腐层(ACL,Anti-CorruptionLayer)。1.共享内核有可能只有一个团队会维护代码、构建和测试共享的内容。共享内核通常一开始就很难构思,也很难维护,因为您必须在团队之间进行公开交流,并就什么构成要共享的模型达成一致。以上是引用《DDD Distilled》(作者VaughnVernon)关于SharedKernel描述的原文。它的优点是Share(减少重复构建),它的缺点也是Share(团队间紧耦合)。2.AnticorruptionLayer(ACL,Anti-CorruptionLayer)一个AnticorruptionLayer是最具防御性的ContextMapping关系,下游团队在它的UbiquitousLanguage(模型)和它上游的UbiquitousLanguage(模型)之间创建一个翻译层.也来自《DDD Distilled》,防腐层是最彻底的隔离方法。它的优点是没有Share(完全解耦,相互独立),缺点是没有Share(有一定的转换成本)。不过,弗农和我的看法相似,都同意防腐层。因为与系统的可维护性和可理解性相比,语义转换增加的成本是完全值得的。只要有可能,您应该尝试在您的下游模型和上游集成模型之间创建一个反腐败层,这样您就可以在您的集成端生成模型概念,专门满足您的业务需求,并使您与外界完全隔离。二方库的搬迁大多数情况下,二方库确实是用来定义服务接口和数据协议的。但是二方库和JSON不同的是,它不仅仅是一个协议,还是一个Java对象,一个Jar包。既然是Java对象,就意味着我们可以让DTO承载更多除了getter和setter之外的功能。这个问题之前没有引起我的注意,但是最近在思考领域模型的时候,发现我们可以让二方库承担更多的责任,发挥更大的作用。其实在阿里,我发现有些团队已经在实践这个了,我觉得效果还不错。比如中台类目二方库就在这件事上做了比较好的示范。分类是商品中比较复杂的逻辑,涉及到很多计算。我们来看看类目二方库的代码是怎么写的:从上面的代码我们可以发现,这已经远远超出了DTO的范畴。它是一个域模型(具有数据、行为和继承)。这样做合适吗?我认为是合适的:首先,DefaultStdCategoryDO使用的所有数据都是自洽的,即这些计算可以自己完成,不需要外界的帮助。比如判断是不是根类,是不是叶类,获取类的名字路径等等,都可以自己完成。其次,这是一个共享内核。我通过第三方库公开我领域的知识(语言、数据和行为)。如果有100个应用需要用到isRoot()来判断,就不用自己实现了。.什么?不是说不推荐共享内核的做法吗?(嗯,孩子有对错,好吧)。我认为这里的共享内核是积极的,尤其是对于类别这样的轻数据、重计算场景。但是共享带来的紧耦合确实是个问题。所以如果我是一个类目服务的消费者,我会选择使用一个Wrapper来对Category进行封装复用,这样它的领域能力可以复用,同时也可以起到隔离和防腐的作用。COLA中的二方库说到这里,我想你应该明白我对二方库的态度了。是的,二方库不应该只是接口和DTO,而是领域的重要组成部分,是实现SharedKernel的重要手段。因此,我打算在COLA2.0中扩大二方库的职责范围。主要包括两点:二方库中的领域模型也是领域的重要组成部分,是一种“轻量级”的领域能力表达。所谓“轻量级”,就是表达式自洽且足够内聚,类似于上面提到的StdCategoryDO的情况。当然能力的表达也需要遵循UbiquitousLanguage。不同限界上下文之间的协作要充分利用二方库的桥梁作用。它们协同工作的方式如下图所示。请注意,这只是建议,而不是标准。事实上,我们总是要在共享和耦合之间做出取舍。世界上没有完美的建筑或完美的设计。适不适合,还是需要根据实际场景自行决定。COLA框架的扩展机制已经走到这一步了,COLA2.0的变化我也差不多解释完了。让我们添加另一个复活节彩蛋。让我知道COLA如何支持扩展作为框架。框架作为一个组件集成在系统中,完成特定的任务。比如logback作为一个日志框架,帮助我们解决打印日志、日志格式、日志存储等问题。但是面对各种各样的应用场景,框架本身是没有办法预知你想要的日志格式和日志归档方式的。这些地方都需要一个扩展机制来赋能用户自己配置和扩展。就扩展的实现而言,一般有两种方式,一种是基于接口的扩展,一种是基于数据配置的扩展。基于接口的扩展基于接口的扩展主要是利用面向对象的多态机制,首先在框架中定义接口(或抽象方法)和处理接口的模板,然后由用户实现自己的定制。原理如下图所示:这种扩展方法在框架中被广泛使用,例如Spring中的ApplicationListener。用户可以实现这个Listener,在容器初始化后做一些特殊的处理。再比如logback中的AppenderBase。用户可以通过继承AppenderBase实现自定义的Appender请求(发送日志到消息队列)。COLA作为一个框架,就有这样的扩展能力。比如我们有一个ExceptionHandlerI,我们在框架中提供了一个默认的实现。代码如下:然而并不是每个应用都愿意做这样的安排,所以我们提供Extension,当用户提供自己的ExceptionHandlerI实现时,优先使用用户的实现。如果用户不提供,则使用默认实现:基于数据配置的扩展基于配置数据。首先,必须约定一种数据格式,然后通过使用用户提供的数据组装成一个实例对象。用户提供的数据是对象中的一个属性(有时可能是一个类,比如slfj中的StaticLoggerBinder)。原理如下图所示:我们在应用中一般使用的KV配置都属于这种形式,在框架中有很多使用场景,比如上面提到的logback中的logback.xml配置日志格式和日志大小.在COLA中,我们使用Annotation配置@Extension(bizId="tmall",useCase="placeOrder",scenario="88vip"),这也是典型的基于数据的配置扩展。COLA2.0源码使用方法COLA2.0源码生成COLA应用https://github.com/alibaba/COLACOLA2.0提供了两套Archetype,一套是purebackendapplication,一套是webbackendapplication,两者的区别就是web后台应用比纯后台应用多了一个Controller模块,其他都一样。我已经将Archetype的二方库上传到MavenRepo,可以通过以下命令生成COLA应用:生成纯后端应用(无Controller)mvnarchetype:generate-DgroupId=com.alibaba.demo-DartifactId=demo-Dversion=1.0.0-SNAPSHOT-Dpackage=com.alibaba.demo-DarchetypeArtifactId=cola-framework-archetype-service-DarchetypeGroupId=com.alibaba.cola-DarchetypeVersion=2.1.0-SNAPSHOT生成Web后端应用程序(带控制器)mvnarchetype:generate-DgroupId=com.alibaba.demo-DartifactId=demo-Dversion=1.0.0-SNAPSHOT-Dpackage=com.alibaba.demo-DarchetypeArtifactId=cola-framework-archetype-web-DarchetypeGroupId=com.alibaba.cola-DarchetypeVersion=2.1。0-SNAPSHOT我们假设新建的应用名为demo,那么执行命令后,会看到如下模块结构,上半部分是应用骨架,下半部分是COLA框架。生成的应用中有一些demo代码,可以直接用“mvntest”进行测试。如果是web后端应用,可以运行TestApplication启动SpringBoot容器,然后直接通过RESTURLhttp://localhost:8080/customer?name=Alibaba访问服务。COLA2.0的整体架构最后,按照老规矩,给出两个全局架构图。让你从全球的角度去把握COLA。注:COLA有两种含义。一个意思是COLA作为一个框架,主要是对一些应用中需要的通用组件提供支持。另一层含义指的是COLA架构,指的是COLAArchetype生成的应用骨架的架构。这里所说的架构视图是应用架构视图。依赖视图调用视图参考:[1]https://softwareengineering.stackexchange.com/questions/178927/is-there-a-difference-between-a-component-and-a-module?spm=ata.13261165.0。0.12296659zlPIXl