文章首发于公众号:松花皮蛋的黑板报
作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解
文章来源:www.liangsonghua.me
作者介绍:京东资深工程师-梁松华,长期关注稳定性保障、敏捷开发、JAVA高级、微服务架构
一、复杂性和规模增长的解决之道
解决复杂和大规模软件的武器可以被粗略地归为三类:抽象、分治和知识
1、分治
把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。即更容易设计各部分的协作方式。评判什么是分治得好,即高内聚低耦合。以火车为例,每个车厢都要符合承重要求,行李车厢承重能力要高于其他车厢,车厢间的连接要牢固且易于拆解,有足够的灵活性方便转弯。另外大件行李应该单独存放,避免占用普通车厢的空间,餐车应该在整个列车的中间,方便用餐
2、抽象
使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐他们需要注意什么
3、知识
通过知识手段抽象出限界上下文以及如何去分治
二、DDD概览
DDD全称为“Domain Driven Design”,意思是领域驱动设计,基于业务知识设计系统,代码反映业务,它将真实业务概念和业务规则转换为软件系统中的概念和规则,在一个个有界上下文中发挥其作用,完成用户要求的功能,从而降低或隐藏业务复杂性,使系统有更好的扩展性,以拥抱变化。说白了你那套领域逻辑到底有没有效?逻辑上自洽为真,没有逻辑矛盾,是不是现实中也是为真?需要在有界上下文中验证
举个例子:假如有父亲和儿子这两个表,生成的 POJO 应该是
public class Father{…}
public class Son{
//son 表里有 fatherId 作为 Father 表 id 外键
private String fatherId;
public String getFatherId(){
return fatherId;
}
……
}
这时候儿子犯了点什么错,老爸非常不爽地扇了儿子一个耳光,老爸手疼,儿子脸疼。Manager通常这么做
public class SomeManager{
public void fatherSlapSon(Father father, Son son){
// 假设 painOnHand, painOnFace 都是数据库字段
father.setPainOnHand();
son.setPainOnFace();
}
}
这里,manager充当了上帝的角色,扇个耳光都得他老人家帮忙。但是现实世界应该是教训儿子是自己的事情,并不需要别人帮忙,上帝也不行
public class Father{
public void slapSon(Son son){
this.setPainOnHand();
son.setPainOnFace();
}
}
三、关注点
1、微服务关注点
1)、运行时进程间通信,能够容错和故障隔离
2)、去中心化管理数据和冶理
3)、服务可以独立的开发、测试、构建、部署
4)、高内聚低耦合,职责单一
2、DDD关注点
1)、关注业务领域,建立边界并构建通用语言,高效沟通
2)、对业务进行抽象,和业务专家一起建模
3)、尽可能维持代码和业务的低表示差异
一个供电系统中,合同部门关心的是电价,用电跟踪部门关心的是电量,到了结算部门则是关心购电量,他们只关心自己所辖边界内的重心,用不同方言在讲话,没有形成统一语言。实际上购电量=电价+电量+用户时间段+用电规则,购电量才是业务模型中的核心方法,它应该是模型中的通用语言。有了统一语言后,可以减少将业务架构映射到系统架构时的层层翻译,避免组件划分过程中的边界错位,增强系统对业务的响应速度
从上面你可以发现DDD强调的是逻辑划分,微服务强调的是隔离部署,系统架构的逻辑划分可以细于部署单元的物理隔离,较好的架构应该是演进式,避免过早微服务化带来的麻烦
四、DDD设计实践
1、按业务划分限界上下文
从业务能力的角度识别核心域、支撑域、通用域并去除二义性,比如电商业务中订单就是核心域,订单服务产生的其他业务则是支撑域,而通知中心、短信服务则是通用域
2、消除隐式数据依赖
一般情况下订单表会使用user_id作为用户标识,但如果系统将来要对接企业,我们可以新增一个订单服务但是开发维护成本过高,可以新增一个企业员工记录但是员工离职交接维护成本过高,可以新增一个虚拟员工账号但是无意间耦合了,正确做法是新增一个企业用户上下文,然后使用”用户ID加用户类型”标识购买者,消除隐式数据依赖
《架构整洁之道》这本书中说到,任何形式的共享数据行为都会导致强耦合,如果给服务之间传递的数据记录中增加一个新字段,那么每个需要操作这个字段的服务都必须要做出相应的变更,服务间必须对这条数据的解读达成一致,那么他们是间接彼此耦合的
3、明确定义依赖方向
这里不得不提到整洁架构(又名洋葱架构),整洁架构最主要原则是依赖原则,它定义了各层的依赖关系,越往里,依赖越低,代码级别越高。外圆代码依赖只能指向内圆,内圆不知道外圆的任何事情。一般来说,外圆的声明(包括方法、类、变量)不能被内圆引用。同样的,外圆使用的数据格式也不能被内圆使用
4、下游的自我保护
对于下流来说,需要根据自己的领域模型创建一个单独的防腐层,该层作为上游系统的代理向你的系统提供功能,它在你自己的模型和他方模型间进行概念对象和其行为进行翻译转换,比如当数据实体格式不符合系统要求时,只需要在防腐层中添加对应的转换器即可,领域模型可保持不变
六、DDD编码的意义
让代码体现业务,保持二者的低表示差异,难点在于对聚合根的实现
在DDD模式中将对象分为值对象和实体。实体对象是有生命周期的,可以唯一标识的(不是数据库中的ID),此对象只能属于某个业务。而值对象是没有生命周期的,比如订单领域上下文中,订单是实体、订单项是实体、订单状态是值对象。原来我们系统划分单位通常是模块,但是粒度不够细,所以需要对实体和值对象等进行关联设计后,进行聚合的划分和聚合根的确定,比如订单和订单项、订单和订单状态有关联,他们整体作为一个聚合,通常聚合中其他实体需要依赖聚合根,很显然订单应该是聚合根(生命周期最长,其他聚合项离开它没有任何意义)。DDD模式中对一个聚合中实体的访问或操作,必须通过这个聚合的聚合根开始,主要的目的是数据的最终一致性。另外聚合根之间不能建立关系,只能通过ID引用
比如
class Book {
private @Id Long id;
private Set<AutherRef> authors = new HashSet<>();
public void addAuther(Author author) {
authers.add(createAuthorRef(author));
}
private AuthorRef createAuthorRef(Author author) {
AuthorRef authorRef = new AuthorRef();
authorRef.author = author.id;
return authorRef;
}
}
七、总结
传统开发模式是确定需求后定义表结构,以数据库表作为系统导向,但是DDD建议应该先着眼于业务本身,减少对数据库表结构的依赖,避免业务发生变化,把我们打得措手不及,它把你的思考起点从技术的角度拉到了业务上。不过在进行DDD设计时需要注意划分边界,注意定义边界间的关系,注意概念不要穿透边界
最后你会发现通篇都在谈论的“边界”划分,我们知道微服务落地的难点之一就是如何正确折分,如果拆分后的服务出现互相调用或者需要高成本解决各个服务间的数据一致性,将得不偿失,所以正确的打开方式是在进行微服务拆分前应该先充分了解领域驱动设计
文章来源:www.liangsonghua.me
作者介绍:京东资深工程师-梁松华,长期关注稳定性保障、敏捷开发、JAVA高级、微服务架构
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。