文章目录
1 引言
软件的首要技术使命:管理复杂度。
工程师的首要技术使命就是控制复杂度。整洁面向对象分层架构(Clean Object-oriented and Layered Architecture,COLA)是阿里团队自主研发的应用架构,是复杂治理之路上的一个里程碑。
COLA不仅是一个架构思想,还提供了一整套可以落地实施的框架和工具。我们可以使用COLA Archetype快速搭建一个符合COLA架构规范的业务应用,并在此基础上快速实现业务功能。
目前,COLA已经开源。自发布以来,COLA得到了社区的普遍关注和认可,并已经在阿里巴巴内外部多个团队落地。从实践情况来看,COLA在应用复杂度治理上的效果显著。
2 软件架构
架构始于建筑,这是人类发展分工协作的需要。将目标系统按某个原则进行切分,切分的原则是便于不同的角色进行并行工作。
软件架构是一个系统的草图。软件架构描述的对象是直接构成系统的抽象组件。各个组件之间的连接则明确和相对细致地描述组件之间的通信。
在实现阶段,这些抽象组件被细化为实际的组件,比如具体某个类或者对象。在面向对象领域中,组件之间的连接通常用接口来实现。
随着互联网的发展,现在的系统要支撑数亿人同时在线购物、通信、娱乐等需要,相应的软件体系结构也变得越来越复杂。软件架构的含义也变得更加宽泛,我们不能简单地用一个软件架构来指代所有的软件架构工作。
3 架构分类
为了更清楚地表述六边形架构、洋葱架构、COLA架构在软件架构中的位置,以及应用开发人员应该关注什么,可以将软件架构划分成业务架构、应用架构、系统架构、数据架构、物理架构和运维架构,如下图所示:
- 业务架构:由业务架构师负责,也可以称为业务领域专家、行业专家。业务架构属于顶层设计,其对业务的定义和划分会影响组织结构和技术架构。
- 应用架构:由应用架构师负责,他需要根据业务场景的需要,设计应用的拓扑结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速地支撑业务发展的同时,确保系统的可用性和可维护性。COLA架构是一个典型的应用架构,致力于应用复杂度的治理。
- 系统架构:根据业务情况综合考虑系统的非功能属性要求(包括性能、安全性、可用性、稳定性等),然后做出技术选型。对于流行的分布式架构系统,需要解决服务器负载、分布式服务的注册和发现、消息系统、缓存系统、分布式数据库等问题,同时解决如何在CAP(Consistency,Availability,Partition Tolerance)定理之间进行权衡的问题。
- 数据架构:对于规模大一些的公司,数据治理是一个很重要的课题。如何对数据收集、处理,提供统一的服务和标准,是数据架构需要关注的问题。其目的就是统一数据定义规范,标准化数据表达,形成有效易维护的数据资产,搭建统一的大数据处理平台,形成数据使用闭环。
- 物理架构:物理架构关注软件元件是如何放到硬件上的,包括机房搭建、网络拓扑结构、网络分流器、代理服务器、Web服务器、应用服务器、报表服务器、整合服务器、存储服务器和主机等。
- 运维架构:负责运维系统的规划、选型、部署上线,建立规范化的运维体系。要借助技术手段控制和优化成本,通过工具化及流程提升运维效率,注重运营效益。制定和优化运维解决方案,包括但不限于柔性容灾、智能调度、弹性扩容与防攻击、推动及开发高效的自动化运维和管理工具、提高运维的自动化程度和效率。
4 典型的应用架构
4.1 分层架构
分层是一种常见的根据系统中的角色(职责拆分)和组织代码单元的常规实践。分层架构大概经历了以下阶段。
- 20世纪60年代~70年代:GUI还没有出现,所有的应用程序要通过命令行使用,那时的应用程序也很简单,还不需要分层,实际上也只有一层。
- 20世纪70年代~80年代:企业应用出现了,用户可以使用计算机通过网路访问应用,开始出现C/S两层架构模式。
- 20世纪90年代之后:互联网开始普及,随着用户的增长,以及应用复杂性和基础实施复杂性的增加,终于诞生了我们现在仍在使用的三层架构,如下图所示:
- 2000年之后:2003年,Eric Evans出版了他的标志性著作《领域驱动设计:软件核心复杂性应对之道》。从此,DDD的概念被人熟悉,以及基于DDD的一系列架构演变开始出现。
可以看到,随着时间的推移,从一层到多层架构,架构的层次越来越多。不过,我们要注意另一个极端——千层面架构。这是一种分层架构的反模式,是一种过渡设计。如果在我们的系统中出现以下情况,就可能有“千层面”的嫌疑:
- 热衷于创建完美的系统导致项目过度抽象。
- 层次太多,增加了整个系统的复杂性。
- 物理层次太多,不但增加了整个系统的复杂性,还降低了系统的性能。
- 严格的分层方法导致上层必须通过中间层次访问,而不是直接访问需要的层次。
4.2 CQRS
命令查询分离(Command Query Separation,CQS)最早是Betrand Meyer(Eiffel语言之父,OCP的提出者)提出的概念,其基本思想在于任何一个对象的方法可以分为以下两类:
- 命令(Command):不返回任何结果(void),但会改变对象的状态。
- 查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。
命令查询职责分离模式(Command Query ResponsibilitySegregation,CQRS)是对CQS模式的进一步改进而成的一种简单架构模式。该模式从业务上分离修改(Command,增、删改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为,从而使得逻辑更加清晰,便于对不同部分进行有针对性的优化。
CQRS使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来,这也意味着在查询和更新过程中使用的数据模型也是不一样的,这样读和写逻辑就隔离开了,如下图所示:
使用了CQRS之后,我们能够把读模型和写模型完全分开,从而可以优化读操作和写操作。除了性能提升,CQRS还让代码库更清晰简洁,更能体现出领域,易于维护。
同样,这全部都是封装、低耦合、高内聚和单一责任原则的体现。
4.3 六边形架构
六边形架构由Alistair Cockburn在2005年提出,解决了传统的分层架构所带来的问题。实际上,六边形架构也是一种分层架构,只不过不是上下,而是内部和外部,如下图所示:
六边形架构又称端口-适配器架构,这个名字更容易理解。六边形架构将系统分为内部(内部六边形)和外部,内部代表应用的业务逻辑,外部代表应用的驱动逻辑、基础设施或其他应用。内部通过端口和外部系统通信,端口代表了一定协议,以API呈现。
一个端口可能对应多个外部系统,不同的外部系统需要使用不同的适配器,适配器负责对协议进行转换。这样就使得应用程序能够以一致的方式被用户、程序、自动化测试、批处理脚本所驱动,并且可以在与实际运行的设备和数据库相隔离的情况下进行开发和测试。
一个端口对应多个适配器,是对一类外部系统的归纳,它体现了对外部的抽象。应用通过端口为外界提供服务,这些端口需要被良好地设计和测试。内部不关心外部如何使用端口,从一开始就要假定外部使用者是可替换的。
适配器的两种不同类型如图所示:
- 左侧代表UI的适配器被称为主动适配器(Driving Adapters),因为是它们发起了对应用的一些操作。
- 右侧表示和后端工具链接的适配器被称为被动适配器(Driven Adapters),因为它们只会对主适配器的操作做出响应。
两种端口-适配器的用法也有一点区别:
- 在左侧,适配器依赖端口,该端口的具体实现会被注入适配器,这个实现包含了用例。换句话说,端口和它的具体实现(用例)都在应用内部。
- 在右侧,适配器就是端口的具体实现,它自己将被注入我们的业务逻辑中,尽管业务逻辑只知道接口。换句话说,端口在应用内部,而它的具体实现在应用之外并包装了某个外部工具。
4.4 洋葱架构
2008年,Jeffrey Palermo提出了洋葱架构(OnionArchitecture)。洋葱架构在端口和适配器架构的基础上贯彻了将领域放在应用中心,将驱动机制(用户用例)和基础设施(ORM、搜索引擎、第三方API等)放在外围的思路。洋葱架构在六边形架构的基础上加入了内部层次。
洋葱架构与六边形架构有着相同的思路,都是通过编写适配器代码将应用核心从对基础设施的关注中解放出来,避免基础设 施代码渗透到应用核心之中。这样应用使用的工具和传达机制都可以轻松地替换,在一定程度上避免技术、工具或者供应商锁定。
洋葱架构分离了基础设施和业务应用,使得我们可以方便地模拟(Mock)基础实施,对业务应用进行测试。企业应用中存在着不止两个层次,洋葱架构还在业务逻辑中加入了一些在领域驱动设计的过程中被识别出来的层次:
- 应用层(Application)
- 领域服务(Domain Service)
- 领域模型(Domain Model)
- 基础设施(Infrastructure)
在洋葱架构中,明确规定了依赖的方向。
- 外层依赖内层
- 内层对外层无感知
也就是说,耦合的方向是从外层指向中心的。洋葱架构提供了一个完全独立的对象模型(领域模型),该模型位于架构的核心,不依赖其他任何层次,我们可以在不影响内层的情况下改变外层的灵活性。洋葱架构在架构层面运用了依赖倒置原则。
4.5 DDD
准确地说,DDD不是架构,而是一种开发思想。就像敏捷不是Scrum,而是一种思想一样。之所以将DDD归类为典型架构,是因为它是我们很多架构的思想来源。比如洋葱架构,其内层(核心业务逻辑)就应该是领域层。当然,COLA也传承了DDD的思想。
另外,DDD带来的最大改变是让我们得以从“数据驱动”转向“领域驱动”,让我们知道领域是应用的核心,其他都是技术细节,随时可以被替换。
5 COLA架构设计
复杂性治理是一个系统化工程,设计COLA的初衷就是要尽一切努力在架构层面去控制软件复杂度。因此,在COLA的设计中,汲取了很多前文中介绍的典型应用架构的优秀思想。可以说,COLA是站在了巨人的肩膀上。然而又不是对前人思想的简单结合,而是融入了很多自己的思考、判断和创新,比如扩展点设计和规范设计。
5.1 分层设计
架构分层是我们在做架构设计时首要考虑的问题。架构上的不合理大多是分层不合理,没有分层或者层次太少,会导致“一锅粥”;层次太多,层次之间又有严格的限制,会导致“千层面”。因此,分层要合理,不能太少,也不能太多。
COLA的分层是一种经过改良的三层架构,主要是将传统的业务逻辑层拆分成应用层、领域层和基础设施层。如图所示,左边是传统的分层架构,右边是COLA的分层架构。
其中,每一层的作用范围和含义如下:
- 展现层(Presentation Layer):负责以Rest的格式接受Web请求,然后将请求路由给Application层执行,并返回视图模型(View Model),其载体通常是数据传输对象(Data Transfer Object,DTO)。
- 应用层(Application Layer):主要负责获取输入、组装上下文、做输入校验、调用领域层做业务处理,当需要时发送消息通知。当然,层次是开放的,若有需要,应用层也可以直接访问基础实施层。
- 领域层(Domain Layer):主要封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Entities)的函数对外部提供业务逻辑的计算和处理。
- 基础设施层(Infrastructure Layer):主要包含数据访问通道(Tunnel)、Config和Common。这里使用Tunnel这个概念对所有的数据来源进行抽象,数据来源可以是数据库(MySQL、NoSQL)、搜索引擎、文件系统,也可以是SOA服务等;Config负责应用的配置;Common是通用的工具类。
采用这样的分层策略,主要是考虑到“业务逻辑”是一个非常宽泛的定义,进一步分析我们会发现,“业务逻辑”可以被分层“核心业务逻辑”和“技术细节”。这正是六边形架构和洋葱架构中提倡的思想,即尽量保证内部核心领域的独立和无依赖,而外部的技术细节可以通过接口和适配器随时更换,从而增加系统的灵活性和可测性。
5.2 扩展设计
只有一个业务的简单场景对扩展性的要求并不突出,这也是扩展设计常被忽略的原因,因为我们大部分的系统都是从单一业务开始的。但是随着业务场景越来越复杂,代码中开始出现大量的if-else逻辑,此时除了常规的策略模式以外,我们可以考虑在架构层面提供统一的扩展解决方案。
在扩展设计中,我们提炼出两个重要的概念,分别是业务身份和扩展点。
业务身份是指在系统唯一标识一个业务或者一个场景的标志。在具体实现中,我们使用BizCode来表示业务身份,采用类似Java包名命名空间的方式。例如,用“ali.tmall”表示阿里巴巴天猫业务,用“ali.tmall.car”表示阿里巴巴天猫的汽车业务,用“ali.tmall.car.aftermarket”表示阿里巴巴天猫的汽车业务的后市场场景。
扩展点的思想源自插件模式。每个业务或者场景都可以实现一个或多个扩展点(ExtensionPoint),也就是说,一个业务身份加上一个扩展点可以唯一地确定一个扩展实现(Extension)。这个业务身份和扩展点的组合,我们称为扩展坐标(ExtensionCoordinate),如图所示。
作为一个框架,COLA在此主要做了两件事情:
- 在系统启动时,扫描注册标记有@Extension的扩展实类
- 在系统Runtime时,根据业务身份选择对应的扩展实现进行执行
5.3 规范设计
任何事物都是规则性和随机性的组合。规范的意义在于可以将规则性的东西固化下来,尽量减少随心所欲带来的复杂度,一致性可以降低系统复杂度。从命名到架构皆是如此,架构本身就是一种规范和约束,破坏这个约束,也就破坏了架构。
COLA制定了一系列的规范,包括组件(Module)结构、包(Package)结构、命名等。
5.3.1 组件规范
COLA规定一个应用至少要有3个组件:应用层、领域层和基础实施层。如果不是严格的前后端分离,也可以加入展现层的组件,但这是可选的。组件之间的依赖关系如图所示。
从上面的依赖关系可以看到,领域组件(Domain Module)是应用的核心,负责核心业务逻辑的处理,不应该有任何的外部依赖。
领域组件的实现方式有两种,一种是把领域组件设计成纯POJO,另一种是通过依赖倒置,将数据访问的接口放在领域组件里,让基础设施组件(Infrastructure Module)去做接口实现。
5.3.2 包规范
相比组件,包是更细粒度的代码组织单元。包的设计也要遵循高内聚、低耦合的原则。每个包都应该是一组功能类似的类的聚集,这种划分使得整个项目形成一个金字塔结构。这种结构化的表达在方便记忆的同时,也使得整个项目结构更加清晰,有章可循。
在一个遵循COLA包规范的应用中,我们可以看到如图所示的包结构。
5.3.3 命名规范
在COLA架构中,我们制定了一系列的命名规范,以便通过名称就能知晓该类的作用和职责范围,从而极大地提升代码的可理解性,提升代码审查(Code Review)的效率。这样如果你将不属于职责范围的代码放在某个类中,做代码审查的人很容易就能识别出来。
规范 | 用途 | 解释 |
---|---|---|
xxxCmd.java | Client Request | Cmd代表Command,表示一个用户请求 |
xxxCO.java | Client Object | 客户对象,用于传递数据,等同于DTO |
xxxServiceI | API Service | API接口类 |
xxxInterceptor.java | Command Interceptor | 拦截器,用于处理切面逻辑 |
xxxExtPt.java | Extension Point | 扩展点 |
xxxExt.java | Extension | 扩展实现 |
xxxValidator.java | Validator | 校验器,用于校验的类 |
xxxConvertor.java | Convertor | 转化器,实现不同层级对象互转 |
xxxAssembler.java | Assembler | 组装器,组装外部服务调用参数 |
xxxE.java | Entity | 代表领域实体 |
xxxV.java | Value Object | 代表值对象 |
xxxRepository.java | Repository | 仓储接口 |
xxxDomainService.java | Domain Service | 领域服务 |
xxxDO.java | Data Object | 数据对象,用于持久化 |
xxxTunnel.java | Data Tunnel | 数据通道,DAO是最常见的通道,也可以是其他通道 |
xxxConstant.java | Constant class | 常量类 |
xxxConfig.java | Configuration class | 配置类 |
xxxUtil.java | Utility class | 工具类 |
6 COLA架构总览
在架构思想上,COLA主张像六边形架构那样,使用端口-适配器去解耦技术细节;主张像洋葱架构那样,以领域为核心,并通过依赖倒置反转领域层的依赖方向。最终形成如图所示的层次关系和依赖关系。
从COLA应用处理响应一个请求的过程来看,COLA使用了CQRS来分离命令和查询的职责,使用扩展点和元数据来提供更高应用的可扩展性,使用ColaMock来提升测试效率。我们可以得到一张如图所示的COLA纵向架构图。
7 小结
本章介绍了一些典型的应用架构,如分层架构、CQRS、六边形架构、洋葱架构和DDD等。融合这些优秀的架构思想,并对其进行建设性的改良和提升是COLA形成的基础。COLA架构是一个相对完善,比较适合解决复杂业务场景问题。因此在使用COLA时,可以根据情况选用相关功能。软件的世界里没有银弹,我们不一定要使用COLA提供的所有能力,可以根据实际情况裁剪使用COLA。
以上内容来源于《代码精进之路:从码农到工匠》核心原理与案例分析。