一、软件复杂性 1、复杂原因怎样的架构才能配得上造到飞起的变化?
如果软件系统存在持续的迭代周期,那么其中业务、技术、架构的复杂性都会直线拉升,其相应的开发难度也会提高,可以用一句话总结其根本原因:唯一不变的就是变化;
- 业务变化:导致复杂性的根本原因,在多端多版本适配的过程中代码快速膨胀;
- 数据变化:数据随着业务的变化和发展,不断沉淀积累,需要做横向与纵向的管理;
- 技术升级:技术组件可能因为漏洞,或者更好的解决问题,不间断升级版本;
- 人员变动:模块的开发人员一旦出现流动,换人接手会给代码带来风格上的差异;
- 心态起伏:持续应对复杂问题,但平稳的心态很难持续,也是人员流动的一个因素;
应对复杂的变化一直都是软件工程的核心难点问题,如何用较小的架构变化应对较大的业务变化,就是设计中常说的:高内聚、低耦合;还需要补充很重要的一点:单从技术层面是无法持续解决复杂问题的,还需要从管理角度去定义流程标准,规范各种解决方案,是整个部门要持续面对的事项。
2、应对复杂不管是常说的设计模式、原则、面向对象,还是架构中常用的集群、微服务、领域驱动等,都是在寻求更合理的方案来应对业务的变化;但是没有一劳永逸的解决方法,既要做一定前瞻性的设计去预期业务,同时还要避免过度的设计影响业务进度;这就需要研发团队具备一定的业务高度和技术深度:
在系统落地的过程中,需要对业务深入的分析和理解,不断优化技术层面的解决方案;比如微服务的思想是通过拆分的手段实现业务块之间的低耦合,领域驱动设计则实现各个业务逻辑的高内聚;下面围绕两种方式的实践去详细分析。
二、微服务架构 1、架构设计系统的架构设计是一件极度复杂的事情,在工作的这几年大致经历过如下几个阶段:单服务、多服务集群、微服务、持续集成;在近2年比较稳定的选型是微服务+自动化集成的模式:
思考其本质的变化逻辑,即为了应对更复杂的业务体系;不管是业务拆分还是模型设计,都是在不断实现高内聚低耦合的原则;降低业务之间的关联影响,分离业务和技术的高度耦合。
2、业务场景这里先来看一个经典的业务场景:电商交易;基于微服务架构的电商交易场景中,通常至少会涉及如下几个核心服务:交易、账户、订单、商品、仓储、物流;
站在业务角度,进行模块化拆分和管理,结合持续集成的组件,通常可以轻松的应对各种复杂的业务场景,但是不存在真正意义上一劳永逸的手段,业务变化带来的各种问题总会无脑推动开发去寻找更合理的解决方案;
在一次完整的电商交易场景中,实际上真正涉及到的微服务远不止图中的几个,在Trade服务中交织关联多个其他服务,在MVC的分层管理下,初期并不会存在较大风险,但是业务一旦经过多版升级改造之后,并且还存在版本兼容的要求,会给人一种极度混乱和不踏实的感觉;
如果团队成员的综合能力较高,并且版本有充足的时间去设计和优化,这种问题是可以妥善解决的,如果出现时间紧任务重的情况,随之而来的压力会持续在开发和测试之间来回横跳;
解决过相关业务场景的研发都知道,重构加持续集成能力,结合严谨的测试,可以应对业务的不断变化;但是在版本兼容的过程中,依然会导致工程中的代码膨胀到飞起,特别是出现中场换人的情况,都会让接手的人员在被埋和离开中,产生一次剧烈的心态挣扎。
3、问题分析在MVC的架构模式中,工程通常会进行如下的分层管理:控制层、服务层、持久层、存储层;服务层在特定复杂的场景中会做细化拆分,比如第三方对接、常用中间件的二次封装:
对于在复杂业务线上争渡的选手来说,对Mvc分层模式的缺陷是深有体会的,Service层聚焦大量复杂的逻辑,通常核心业务块中总会存在几个代码过千行的实现逻辑,不管用什么思路和模式去拆分封装,都很难解决该层不断扩展带来的膨胀问题。
4、面向过程在MVC分层中,过程式的代码极其明显,通常以数据库表和关系为基础,映射构建相关实体对象,这些实体对象并没有具体的行为和逻辑,只是作为数据和结构的载体:
从面向对象中类的定义去看:属性和行为;而在MVC模式中,绝大多数实体都只是作为数据的入参出参的结构定义,可以理解为数据容器,在MVC的各层之间不断搬运和加工。
三、领域驱动设计相比MVC的分层设计,领域驱动设计(Domain-Driven-Design简称DDD)对于复杂业务系统的实现,提出了更加合理的解决方案,DDD模式中涉及大量专业术语和抽象概念,可以参考EricEvans
的相关书籍,本文只描述实践中的核心概念。
DDD模型在分层设计上,划分出核心的四层:接入层、应用层、领域层、基础设施层;注意这里只是单纯站在服务端的常规架构角度去看,很明显分离MVC模式中的服务实现层的逻辑:
其中领域层是关键所在,用来封装复杂的业务,对应用层提供业务管理的核心支撑;整个模型也更具备纵向思维,有效的缓解单层复杂度过高的现象;单从模型设计上看,在工程中基于该分层去管理代码包,也可以使每层的设计更加清晰和独立。
2、设计思想领域驱动设计并不是简单的分层管理模型,涉及诸多抽象逻辑与专业术语,例如:领域、界限上下文、实体、聚合、值对象等等;
2.1 领域
领域可以理解为业务场景中需要解决的问题合集,是具有范围和边界的约束;领域可以拆分多个子域,通常描述为:核心域、支撑域、通用域:
关于子域的划分也是参考业务属性,可以把核心域理解为最关键的业务场景,并且需要资源倾斜以应对其不断的发展;支撑域可以理解为相对稳定的业务;通用域偏向系统架构层面的公共能力;通过对领域的拆分实现业务分治,这与微服务的拆分思想相符合,两种模式在业务角度是比较统一的;
2.2 界限上下文
DDD中最晦涩难懂的一个抽象概念,特定模型的限界应用,不过可以借用原文的比喻会意一下:细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外, 并且确定了什么物质可以通过细胞膜:
界限上下文的定义涉及粒度的思想,即每个粒度要具备独立性;如上图仓储业务,可以将服务部署与仓储子域、仓储上下文做成一一对应的关系,或者在仓储子域中分别定义:仓库和货架两个上下文;这里具有极大的灵活性,没有真正意义上的标准可以参考。
2.3 映射关系
做好界限上下文的划分,理清各个上下文之间的关系,明确业务场景中的依赖顺序,这样可以更好的推动开发流程的落地;对于上下文的关系描述也远不止图中的这些,还有共享内核、合作等等:
- 上下游(U-上游,D下游):描述上下文调用时的关系,服务调用方为D,服务提供方为U;
- 防腐层(Anticorruption-Layer,简写ACL):上下文交互时封装的一层,提供对动作的校验、适配、转换等;
- 开放主机服务,发布语言(Open-Host-Service简写OHS,Published-Language简写PL):定义访问协议;
在上下文交互时,防腐层可以维护上下文的隔离和独立,确保调用方不直接依赖服务提供方,从而实现不同上下文之间的依赖解耦;同时这也会带来大量的对象转换动作;
2.4 建模设计
子域和界线上线文完成对业务的拆分切块,从而进行分治;基于防腐层降低各个界限上下文的耦合程度;聚合思想保证了业务问题的解决方案内聚;严格的分层模型实现服务支撑能力的分散;
- 防腐层(Anticorruption-Layer):上下文交互时封装的一层;
- 领域层(Domain-Layer):在分层架构中负责领域逻辑的设计和实现;
- 领域服务(Domain-Service):行为无法识别归属的实体时,封装到领域服务;
- 聚合(Aggregate):相关对象的集合,描述核心领域,通常把聚合作为数据修改的单元;
- 实体(Entity):通过标识来定义的对象,而不是基于属性,比如Uid标识用户实体;
- 值对象(Value-Object):描述特征或属性但没有标识的对象;
- 工厂(Factory):封装对象复杂的创建逻辑与类型;
- 存储库(Repository):把存储、缓存、搜索等资源封装的机制,对应领域模型;
领域模型的核心追求目标:高内聚、低耦合;更加抽象的、复杂的设计思想,也同样意味着落地实现的难度更高,但不可否认领域模型作为复杂业务的解决方案,逻辑上的确更加合理。
3、工程实践领域模型在代码工程的实践中,可以将不同的子域集成到各自的服务中,也可以在一个服务中,通过多个模块(Module)进行隔离维护,即一个模块对应一个界限上下文;
将业务问题进行分模块分层分包的方式进行隔离,是代码工程中的基本手段,这里只是对组织方式进行描述,在实际的开发中,要根据依赖顺序进行类库拆包管理;
在程序的执行过程中,并不是所有的交互命令都需要经过领域层,实际上大部分业务中的查询命令都是超过增删改命令的,所以在纯读取数据的请求中,应用层可以绕开领域层直接访问基础设施层,减少一层数据处理逻辑。
四、实践总结最后来讨论一些架构实践的经验,随着技术的不断发展和更新换代,为解决业务问题提供了极大的便利,不管是单服务中各种成熟的组件,又或者分布式中的微服务体系,或者聚焦业务管理的领域模型;每种架构选型都有其适用的场景,不同的选型意味着不一样的实现成本;
实际上在做架构选型时,成熟有经验的主导者,都极其擅长做折中处理,也就是常说的退一步海阔天空;通常需要考虑团队的综合水平与业务需求和产品设计,当然在实际的协作流程中多方都是需要相对让步的,但是对质量的要求以及核心业务的实现逻辑上是不能打折的。
五、参考源码编程文档:
https://gitee.com/cicadasmile/butte-java-note
应用仓库:
https://gitee.com/cicadasmile/butte-flyer-parent