业务初期,功能比较简单,CRUD基本可以满足。但随着系统的不断演化,业务系统越来越复杂,各模块间有着千丝万缕的关系,如何提升其扩展性,避免牵一发而动全身,是我们非常关心的。
我们会想到重构,重构伴随在业务迭代的整个生命周期里。保持行为不变的代码改善清除了不协调的局部设计。克服演进式设计中大杂烩问题的主力,通过在单独的类及方法级别上做一系列小步重构来完成。
早年的J2EE开发模式,讲究 Web/Service/Dao 三层结构。面向过程编程,对象只是数据的载体,没有行为。以数据为中心,以数据库ER设计作驱动。分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。该对象我们称之为贫血领域对象。
贫血领域对象(Anemic Domain Object):是指仅用作数据载体,而没有行为和动作的领域对象。大量的业务逻辑写在了Service层中,随着业务逻辑复杂,业务逻辑、状态会散落在Service层中的很多处理类或方法中。将数据和行为割裂,原来的代码意图会越来越模糊,代码的理解和维护成本会越来越高。
如何设计复杂的业务系统
概要来讲分为三块:拆分、抽象、DDD
1、拆分。分为业务维度、技术维度。
业务维度把大的问题域拆分成若干小的业务子域。这样容易实现人员、资源的聚焦。但要考虑如何拆分的合理性,注重高内聚低耦合。
技术维度主要是软件分层,如MVC,讲究的模块化、组件化,预留接口,支持扩展。
2、抽象。物质决定意识,万事万物都有原型。根据达尔文的进化论,万物都有自己的特征,相似的物种可以归类到一个属、科等。软件也是一样的道理,将相似的业务聚拢,底层模型统一化设计,并要支持好扩展性。
3、DDD。至少30年以前,一些软件设计人员就已经意识到领域建模和设计的重要性,并形成一种思潮,Eric Evans将其定义为领域驱动设计(Domain-Driven Design,简称DDD),将数据和行为封装在一起,并与现实世界中的业务对象相映射。各类具备明确的职责划分,将领域逻辑分散到领域对象中。
DDD核心组成
1、实体
当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。
例:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)
2、值对象
当一个对象用于对事物进行描述而没有唯一标识时,它被称作值对象(Value Object)。
例:比如颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”}这样的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。
3、聚合根
Aggregate(聚合)是一组相关对象的集合,作为一个整体被外界访问,聚合根(Aggregate Root)是这个聚合的根节点。
聚合由根实体,值对象和实体组成。如:一个电脑包含硬盘、CPU、内存条等,这一个组合就是一个聚合,而电脑就是这个组合的聚合根。在聚合中,根是唯一允许外部对象保持对它的引用的元素,而边界内部的对象之间则可以互相引用。
4、领域服务
当我们在分析某一领域时,一直在尝试如何将信息转化为领域模型,但并非所有的点我们都能用Model来涵盖。对象应当有属性,状态和行为,但有时领域中有一些行为是无法映射到具体的对象中的,我们也不能强行将其放入在某一个模型对象中,而将其单独作为一个方法又没有地方,此时就需要服务。
服务是无状态的,对象是有状态的。所谓状态,就是对象的基本属性:高矮胖瘦。服务本身也是对象,但它却没有属性(只有行为),因此说是无状态的。
5、领域事件
领域事件是对领域内发生的活动进行的建模。
6、限界上下文
一个由显示边界限定的特定职责。领域模型便存在于这个边界之内。在边界内,每一个模型概念,包括它的属性和操作,都具有特殊的含义。
一个给定的业务领域会包含多个限界上下文,想与一个限界上下文沟通,则需要通过显示边界进行通信。系统通过确定的限界上下文来进行解耦,而每一个上下文内部紧密组织,职责明确,具有较高的内聚性。
一个很形象的隐喻:细胞质所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。
如何划分限界上下文?
我们的实践是,考虑产品所讲的通用语言,从中提取一些术语称之为概念对象,寻找对象之间的联系;或者从需求里提取一些动词,观察动词和对象之间的关系;我们将紧耦合的各自圈在一起,观察他们内在的联系,从而形成对应的界限上下文。形成之后,我们可以尝试用语言来描述下界限上下文的职责,看它是否清晰、准确、简洁和完整。简言之,限界上下文应该从需求出发,按领域划分。
7、资源库(Repositories)
资源库的是封装所有获取对象引用所需的逻辑。领域对象不需处理基础设施,以得到领域中对其他对象的所需的引用。只需从资源库中获取它们,于是模型重获它应有的清晰和焦点。
资源库会保存对某些对象的引用。当一个对象被创建出来时,它可以被保存到资源库中,然后以后使用时可从资源库中检索到。如果客户程序从资源库中请求一个对象,而资源库中并没有它,就会从存储介质中获取它。换种说法是,资源库作为一个全局的可访问对象的存储点而存在。
Repository的接口应当采用领域通用语言。作为客户端,不应当知道数据库实现的细节。
Repository和DAO的作用类似,二者的主要区别:
DAO是比Repository更低的一层,包含了如何从数据库中提取数据的代码。
Repository以“领域”为中心,所描述的是“领域语言”。Repository把ORM框架与领域模型隔离,对外隐藏封装了数据访问机制。
DDD四层架构
1、User Interface 接口层,主要用于处理用户发送的Restful请求、解析用户输入信息、校验等,并将信息传递给Application层的接口。
2、Application为应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其它系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
3、Domain为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心,领域模型位于这一层。
4、Infrastructure为基础设施层,为其他层提供通用的技术能力:业务平台,编程框架,持久化机制,消息机制,第三方库的封装,通用算法,等等
DDD六边形架构
DDD分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于抽象,整个分层架构好像被推平了。如果我们把分层架构推平,再向其中加入一些对称性,就会出现一种具有对称性特征的架构风格,即六边形架构。六边形架构是Alistair Cockburn在2005年提出的,在这种架构中,不同的客户通过“平等”的方式与系统交互。需要新的客户吗?不是问题。只需要添加一个新的适配器将客户输入转化成能被系统API所理解的参数就行。同时,对于每种特定的输出,都有一个新建的适配器负责完成相应的转换功能。
DDD和微服务什么关系
随着微服务架构的不断流行,很多企业开始在自己的业务中推行微服务。但在实际落地过程中,总是遇到各种边界问题。DDD中的限界上下文则完美匹配微服务要求,可以将该限界上下文理解为一个微服务进程。DDD在面向高度复杂的软件系统,如何去建模,它的核心点是根据系统的复杂度建立合适的模型。
DDD 的一个生命周期是这样的:在设计和实现一个系统的时候,业务领域专家和开发人员以一套统一语言进行协作,共同完成领域模型的构建,在这个过程中,业务架构和系统架构等问题都得到了解决,之后将领域模型中关于系统架构的主体映射为实现代码,完成系统的实现落地。
DDD的本质是一种软件设计方法,而微服务是具体的落地实现。
在微服务架构实践中,人们大量地使用了DDD中的概念和技术,注意事项
1、微服务中应该首先建立UL(Ubiquitous Language,通用语言),然后再讨论领域模型。
2、一个微服务最大不要超过一个BC(Bounded Context,限界上下文),否则微服务内会存在有歧义的领域概念。
3、一个微服务最小不要小于一个聚合,否则会引入分布式事务的复杂度。
4、微服务的划分过程类似于BC的划分过程,每个微服务都有一个领域模型。
5、微服务间的集成可以通过Context Map来完成,比如ACL(Anticorruption Layer,防腐层)。
6、微服务间最好采用Domain Event(领域事件)来进行交互,使得微服务可以保持松耦合。