推荐好文:
2.5万字详解23种设计模式
代码中如何干掉太多的if else即if else的多种替代方案以提高代码质量通过公司代码审查
微服务springcloud环境下基于Netty搭建websocket集群实现服务器消息推送----netty是yyds
小编先问大家一个问题,也算是高级工程师面试中常问的问题,怎么样才能设计出一个好的软件系统,或者说一个高质量的大型软件系统应该具有哪些特点?
欢迎大家在评论区积极留言探讨,或者关注【微信公众号】微信搜索【老板来一杯java】,公众号回复【java】即可获取【java基础经典面试】一份!【加群后】即可获取【DDD领域驱动设计实战落地解惑】PDF一份!回复【进群】即可进入无广告交流群!
DDD也是设计一个高质量软件系统中的一种解决方案。了解DDD之前,小编建议读者还是需要具备一定的设计模式的思想,不太了解设计模式的可以先参考小编的文章:
2.5万字详解23种设计模式
DDD这个思想呢,最早是Eric Evans(埃里克·埃文斯)在2003年《Domain-Driven Design –Tackling Complexity in the Heart of Software》书中提出的一个概念,该书翻译过来就是领域驱动设计—软件核心复杂性应对之道,但是提出的时候微服务当时并没有流行,所以一直没有火起来,DDD最近开始流行的原因,主要是借着微服务的东风。
MVC三层开发模式大家应该都非常熟悉,现在公司开发基本都是这种模式。
MVC开发流程:
这一步步下来,是不是感觉非常的丝滑。比如产品提了一个需求,首先我们一般会想考虑设计几张表,怎么存储数据,然后建立dao层,service层,controller层来实现这个功能。但是严格来讲,mvc本质上是一种面向数据的设计,主要关注数据,自低向上的思想。虽然在开发速度上有一定优势,如果只追求开发速度,面向数据模型编程在短期之内可以搞定需求,但一味追求速度,如果你系统的业务变化快速,从长远来看随着时间的增长,系统堆了杂七杂八以后,MVC的短板就会日益明显。
1.新需求的开发会越来越难。
2.代码维护越来越难,一个类代码太多,这怎么看对吧,就是一堆屎山。
3.技术创新越来越难,代码没时间重构,越拖越烂。
4.测试越来越难,没办法单元测试,一个小需求又要回归测试,太累。
单体架构局部业务膨胀可以拆分成微服务,微服务架构局部业务膨胀,又拆成什么呢?
DDD就是为了解决这些问题的存在,从一个软件系统的长期价值来看,就需要用DDD,虽然一开始从设计到开发需要成本,但是随着时间的增长,N年以后代码依然很整洁,利于扩展和维护,高度自治,高度内聚,边界领域划分的很清楚。当然了,针对于简单的系统用DDD反而用复杂了,杀鸡焉用宰牛刀!
MVC的开发模式:是数据驱动,自低向上的思想,关注数据。
DDD的开发模式:是领域驱动,自顶向下,关注业务活动。
DDD 分层架构中的要素其实和三层架构类似,只是在 DDD 分层架构中,这些要素被重新归类,重新划分了层,确定了层与层之间的交互规则和职责边界。
MVC是一个短暂的快乐但不足以支撑漫长的生活,DDD是一个不要短暂的温存而是一世的陪伴,如果是你来抉择你会选择哪一个?
DDD(Domain Driven Design)领域驱动模型,是一种处理高度复杂领域的设计思想,不是一种架构,而是一种架构设计方法论,是一种设计模式。说白了就是把一个复杂的软件应用系统的其中各个部分进行一个很好的拆解和封装,以达到高内聚低耦合的这样一个效果。
说白了就是,DDD就是以高内聚低耦合为目的,对软件系统进行模块化的一种思想。
指的是领域名词、动词分析、提取领域模型。官方解释,在某个领域,核心围绕上下文的设计,主要关注上下文的划分、上下文映射的设计,通用语言的设计。
说白了就是,在某个系统,核心围绕子系统的设计;主要关注,这些子系统的划分,子系统的交互方式,还有子系统的核心术语的定义。
用领域模型指导设计及编码的实现。官方解释,核心关注上下文中的实体建模,定义值对象,实体等,更偏向开发细节。
说白了就是,上下文对应的就是某一个子系统,子系统里代码实现怎么设计,就是战术设计要解决的问题。核心关注某个子系统的代码实现,以面向对象的思维设计类的属性和方法,和设计类图没有什么区别,只是有一些规则而已。就是
指导我们划分类。
问题空间属于需求分析阶段,重点是明确这个系统要解决什么问题,能够提供什么价值,也就是关注系统的What与Why。
问题空间将问题域提炼成更多可管理的子域,是真对于问题域而言的。问题空间
在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。
领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围,每个子域又包含了核心子域,支撑子域,通用子域。
领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小服务需要解决的问题域,构建合适的领域模型。
说白了就是,就是系统下面有多个子系统,就是分了一些类型,比如电商系统,订单就是核心领域,支付调用银行,支付宝什么的就是支撑子域,相当于我们俗称的下游,通用子域,就是一些鉴权,用户中心,每个系统都会用到,就设计成通用子域,关键就是讨论过程如何得出这些问题域,是战略设计要解决的。
解决方案域属于系统设计阶段,针对识别出来的问题域,寻求合理的解决方案,也就是关注系统的How。在领域驱动设计中,核心领域(Core Domain)与子领域(Sub Domain)属于问题域的范畴,限界上下文(Bounded Context)则属于解决方案域的范畴。
说白了就是,得出这些问题域之后,就基于这些问题域来求解,属于解决空间。相当于,知道了y=2x,知道了x是多少,然后求y的值。解决空间就是指,领域之间的关系是什么样子,每个领域中通用的术语 ,具体在领域内怎么实现代码,进行领域建模就可以了。
从问题域到解决方案域,实际上就是从需求分析到设计的过程,也是我们逐步识别限界上下文的过程。
事件风暴的基本思想,就是将软件开发人员和领域专家聚集在一起,完成领域模型设计(领域分析和领域建模)。划分出微服务逻辑边界和物理边界,定义领域模型中的领域对象,指导微服务设计和开发。
领域分析,是根据需求划分出初步的领域和限界上下文,以及上下文之间的关系;然后分析每个上下文内部,抽取每个子域的领域概念,识别出哪些是实体,哪些是值对象;
领域建模,就是对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根;
DDD需要进行领域分析和领域建模,除了事件风暴之外实现的方法有,领域故事讲述,四色建模法,用例法等。
事件风暴是建立领域模型的主要方法,但是在 DDD 领域建模和系统建设过程中,有很多的参与者,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等。对同样的领域知识,不同的参与角色可能会有不同的理解,那大家交流起来就会有障碍,怎么办呢?因此,在 DDD 中就出现了“通用语言”和“限界上下文”这两个重要的概念。
DDD的主要参与者:领域专家+开发人员。领域专家擅长某个领域的知识,专注于交付的业务价值。而开发人员则注重于技术实现,总是想着类、接口、方法、设计模式、架构等。这也就导致了团队交流的困难性。因此找到双方的通用语言是解决该问题的有效途径。
通用语言定义上下文含义。在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。
通用语言包含术语和用例场景,并且能够直接反映在代码中。通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。
通用语言说白了就是,使用团队中大家都懂的概念,解决交流障碍的问题,使领域专家和开发人员能够协同合作,从而能够确保业务需求的正确表达。
官方解释:限界上下文主要用来封装通用语言和领域对象。
限界上下文可以拆分为两个词,限界和上下文。
限界:适用的对象一般是抽象事物,指不同事物的分界,指定某些事物的范围。
上下文:个人理解就是语境。语言都有它的语义环境,同样,通用语言也有它的上下文环境。为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。限界上下文就是用来定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。
比如说,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。那么,领域边界就是通过限界上下文来定义的。
限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
限界上下文是业务概念的边界,是业务问题最小粒度的划分。在某个业务领域中会包含多个限界上下文,我们通过找出这些确定的限界上下文对系统进行解耦,要求每一个限界上下文其内部必须是紧密组织的、职责明确的、具有较高的内聚性。说白了就是,上下文对应的就是某一个子系统,系统之间要划分好边界。
上下文之间交互方式就是上下文映射,相对于系统里面这就是RPC,http等交互方式。
从广义上讲,领域具体指一种特定的范围或区域。在DDD中上下文的划分完的东西叫作领域,领域下面又划分了,核心领域,支撑子域,通用子域。
子域:在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。
核心域:它是业务成功的主要因素和公司的核心竞争力
通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域
支撑域:有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,就是支撑域。
说白了就是,系统下面有多个子系统,就是分了一些类型,比如电商系统,订单就是核心领域,支付调用银行,支付宝什么的就是支撑子域,相当于我们俗称的下游,通用子域,就是一些鉴权,用户中心,每个系统都会用到,就设计成通用子域,关键就是讨论过程如何得出这些域,是战略设计要解决的。
领域模型是对领域内的概念类或现实世界中对象的可视化表示。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。
是描述业务用例实现的对象模型。它是对业务角色和业务实体之间应该如何联系和协作以执行业务的一种抽象。
领域模型分为领域对象和领域服务两大类,领域对象用于存储状态,领域服务用于改变领域对象的状态。
特点:
聚合之间产生的业务协同使用领域事件的方式来完成,领域事件就是将上游聚合处理完成这个动作通过事件的方式进行抽象。
在DDD中有一个原则,一个业务用例对应一个事务,一个事务对应一个聚合根,也就是在一次事务中只能对一个聚合根操作。但在实际应用中,一个业务用例往往需要修改多个聚合根,而不同的聚合根可能在不同的限界上下文中,引入领域事件即不破坏DDD的一个事务只修改一个聚合根的原则,也能实现限界上下文之间的解耦。对于领域事件发布,在领域服务发布,在不使用领域服务的情况下,则由应用层在调用资源库持久化聚合根之后再发布领域事件。
一个事件可能当前限界上下文内也需要消费,即可能有多个限界上下文需要消费,一个事件对应多个消费者。
一个完整的领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。
事件发布:构建一个事件,需要唯一标识,然后发布;
事件存储:发布事件前需要存储,因为接收后的事建也会存储,可用于重试或对账等;就是每次执行一次具体的操作时,把行为记录下来,执行持久化。
事件分发:服务内的应用服务或者领域服务直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等,支持同步或者异步。
事件处理:先将事件存储,然后再处理。
当然了,实际开发中事件存储和事件处理不是必须的。
因此实现方案:发布订阅模式,分为跨上下文(kafka,RocketMq)和上下文内(spring事件,Guava Event Bus)的领域事件。
e.g. 用户注册后,发送短信和邮件,使用spring事件实现领域事件代码如下:
1.创建用户注册事件
/**
* 用户注册事件
* @Author WDYin
**/
public class UserRegisterEvent extends ApplicationEvent {
public UserRegisterEvent(Object source) {
super(source);
}
}
2.用户监听事件
/**
* 用户监听事件
* @Author WDYin
**/
@Component
public class UserListener {
@EventListener(UserRegisterEvent.class)
public void userRegister(UserRegisterEvent event) {
User user = (User) event.getSource();
System.out.println("用户注册。。。发送短信。。。" + user);
System.out.println("用户注册。。。发送邮件。。。" + user);
}
@EventListener(UserCancelEvent.class)
public void userCancelEvent(UserCancelEvent event) {
User user = (User) event.getSource();
System.out.println("用户注销。。。" + user);
}
}
3.发布用户注册事件
/**
* 发布用户注册事件
* @Author : WDYin
*/
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class MyClient {
@Autowired
private ApplicationContext applicationContext;
@Test
public void test() {
User user = new User();
//发布事件
applicationContext.publishEvent(new UserRegisterEvent(user));
}
}
官方解释,实体和值对象会形成聚合,每个聚合一般是在一个事务中操作,一般都有持久性操作。聚合中,跟实体的生命周期决定了聚合整体的生命周期。说白了,就是对象之间的关联,只是规定了关联对象规则(必须是由实体和值对象组成的),操作聚合时类似hibernate的One-Many对象的操作,一起操作,不能单独操作。
e.g. 权限管理系统——用户实体,代码如下:
@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {
/**
* 用户id-聚合根唯一标识
*/
private UserId userId;
/**
* 用户名
*/
private String userName;
/**
* 姓名
*/
private String realName;
/**
* 手机号
*/
private String phone;
/**
* 密码
*/
private String password;
/**
* 锁定结束时间
*/
private Date lockEndTime;
/**
* 登录失败次数
*/
private Integer failNumber;
/**
* 用户角色
*/
private List<Role> roles;
/**
* 部门
*/
private Department department;
/**
* 领导
*/
private User leader;
/**
* 下属
*/
private List<User> subordinationList = new ArrayList<>();
/**
* 用户状态
*/
private UserStatus userStatus;
/**
* 用户地址
*/
private Address address;
public User(String userName, String phone, String password) {
saveUserName(userName);
savePhone(phone);
savePassword(password);
}
/**
* 保存用户名
* @param userName
*/
private void saveUserName(String userName) {
if (StringUtils.isBlank(userName)){
Assert.throwException("用户名不能为空!");
}
this.userName = userName;
}
/**
* 保存电话
* @param phone
*/
private void savePhone(String phone) {
if (StringUtils.isBlank(phone)){
Assert.throwException("电话不能为空!");
}
this.phone = phone;
}
/**
* 保存密码
* @param password
*/
private void savePassword(String password) {
if (StringUtils.isBlank(password)){
Assert.throwException("密码不能为空!");
}
this.password = password;
}
/**
* 保存用户地址
* @param province
* @param city
* @param region
*/
public void saveAddress(String province,String city,String region){
this.address = new Address(province,city,region);
}
/**
* 保存用户角色
* @param roleList
*/
public void saveRole(List<Role> roleList) {
if (CollectionUtils.isEmpty(roles)){
Assert.throwException("角色不能为空!");
}
this.roles = roleList;
}
/**
* 保存领导
* @param leader
*/
public void saveLeader(User leader) {
if (Objects.isNull(leader)){
Assert.throwException("leader不能为空!");
}
this.leader = leader;
}
/**
* 增加下属
* @param user
*/
public void increaseSubordination(User user) {
if (null == user){
Assert.throwException("leader不能为空!");
}
this.subordinationList.add(user);
}
}
###14. 值对象:
官方解释,描述了领域中的一件东西,将不同的相关属性组合成了一个概念整体,当度量和描述改变时,可以用另外一个值对象予以替换,属性判等,固定不变。
说白了就是,不关心唯一值,具有校验逻辑,等值判断逻辑,只关心值的类。只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。比如下单的地址。
当你决定一个领域概念是否是一个值对象时,需考虑它是否拥有以下特征:
值对象本质上就是一个集。该集合有若干用于描述目的、具有整体概念和不可修改的属性。该集合存在的意义是在领域建模的过程中,值对象可保证属性归类的清晰和概念的完整性,避免属性零碎。
代码如下:
/**
* 地址数据
*
* @Author WDYin
* @Date 2022/5/24
*/
@Getter
public class Address extends ValueObject {
/**
* 省
*/
private String province;
/**
* 市
*/
private String city;
/**
* 区
*/
private String region;
public Address(String province, String city, String region) {
if (StringUtils.isBlank(province)){
Assert.throwException("province不能为空!");
}
if (StringUtils.isBlank(city)){
Assert.throwException("city不能为空!");
}
if (StringUtils.isBlank(region)){
Assert.throwException("region不能为空!");
}
this.province = province;
this.city = city;
this.region = region;
}
}
官方解释,实体和值对象会形成聚合,每个聚合一般是在一个事务中操作,一般都有持久性操作。聚合中,跟实体的生命周期决定了聚合整体的生命周期。说白了,就是对象之间的关联,只是规定了关联对象规则(必须是由实体和值对象组成的),操作聚合时类似hibernate的One-Many对象的操作,一起操作,不能单独操作。
聚合的规范:
在DDD中,聚合也可以用来表示整体与部分的关系,但不再强调部分与整体的独立性。聚合是将相关联的领域对象进行显示分组,来表达整体的概念(也可以是单一的领域对象)。比如将表示订单与订单项的领域对象进行组合,来表达领域中订单这个整体概念。
聚合根(Aggreate Root, AR)就是软件模型中那些最重要的以名词形式存在的领域对象。聚合根是主要的业务逻辑载体,DDD中所有的战术实现都围绕着聚合根展开。70%的场景下,一个聚合内都只有一个实体,那就是聚合根。
说白了就是:聚合的根实体,最具代表性的实体。比如订单和订单项聚合之后的聚合根就是订单。
聚合根的特征:
通过事件风暴(我理解就是头脑风暴,不过我们一般都是先通过个人理解,然后再和相关核心同学进行沟通),得到实体和值对象;
将这些实体和值对象聚合为“投保聚合”和“客户聚合”,其中“投保单”和“客户”是两者的聚合根;
找出与聚合根“投保单”和“客户”关联的所有紧密依赖的实体和值对象;
在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。
贫血模型具有一堆属性和set get方法,存在的问题就是通过pojo这个对象上看不出业务有哪些逻辑,一个pojo可能被多个模块调用,只能去上层各种各样的service来调用,这样以后当梳理这个实体有什么业务,只能一层一层去搜service,也就是贫血失忆症,不够面向对象。
代码如下:
public class User {
private Long id;
private String userName;//用户名
private String password;//密码
private String gesture; //手势密码
private String phone; //手机号码
private String email;
private int status; //账户状态
private Date lockEndTime; //锁定结束时间
private int failNumber; //登录失败次数
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getGesture() {
return gesture;
}
public void setGesture(String gesture) {
this.gesture = gesture;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getLockEndTime() {
return lockEndTime;
}
public void setLockEndTime(Date lockEndTime) {
this.lockEndTime = lockEndTime;
}
public int getFailNumber() {
return failNumber;
}
public void setFailNumber(int failNumber) {
this.failNumber = failNumber;
}
}
比如如下user用户有改密码,改手机号,修改登录失败次数等操作,都内聚在这个user实体中,每个实体的业务都是清晰的,就是充血模型,充血模型的内存计算会多一些,内聚核心业务逻辑处理。
说白了就是,不只是有贫血模型中setter getter方法,还有其他的一些业务方法,这才是面向对象的本质,通过user实体就能看出有哪些业务存在。
代码如下:
@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {
/**
* 用户名
*/
private String userName;
/**
* 姓名
*/
private String realName;
/**
* 手机号
*/
private String phone;
/**
* 密码
*/
private String password;
/**
* 锁定结束时间
*/
private Date lockEndTime;
/**
* 登录失败次数
*/
private Integer failNumber;
/**
* 用户角色
*/
private List<Role> roles;
/**
* 部门
*/
private Department department;
/**
* 用户状态
*/
private UserStatus userStatus;
/**
* 用户地址
*/
private Address address;
public User(String userName, String phone, String password) {
saveUserName(userName);
savePhone(phone);
savePassword(password);
}
/**
* 保存用户名
* @param userName
*/
private void saveUserName(String userName) {
if (StringUtils.isBlank(userName)){
Assert.throwException("用户名不能为空!");
}
this.userName = userName;
}
/**
* 保存电话
* @param phone
*/
private void savePhone(String phone) {
if (StringUtils.isBlank(phone)){
Assert.throwException("电话不能为空!");
}
this.phone = phone;
}
/**
* 保存密码
* @param password
*/
private void savePassword(String password) {
if (StringUtils.isBlank(password)){
Assert.throwException("密码不能为空!");
}
this.password = password;
}
/**
* 保存用户地址
* @param province
* @param city
* @param region
*/
public void saveAddress(String province,String city,String region){
this.address = new Address(province,city,region);
}
/**
* 保存用户角色
* @param roleList
*/
public void saveRole(List<Role> roleList) {
if (CollectionUtils.isEmpty(roles)){
Assert.throwException("角色不能为空!");
}
this.roles = roleList;
}
}
聚合根与领域服务负责封装实现业务逻辑。领域服务负责对聚合根进行调度和封装,同时可以对外提供各种形式的服务,对于不能直接通过聚合根完成的业务操作就需要通过领域服务。
说白了就是,聚合根本身无法完全处理这个逻辑,例如支付这个步骤,订单聚合不可能支付,所以在订单聚合上架一层领域服务,在领域服务中实现支付逻辑,然后应用服务调用领域服务。
在以下几种情况时,我们可以使用领域服务:
遵守以下规范:
e.g. 用户升职,上级领导要变,上级领导的下属要变
代码如下:
/**
* @Author WDYin
* @Date 2022/5/15
**/
@Service
public class UserDomainServiceImpl implements UserDomainService {
@Override
public void promote(User user, User leader) {
//保存领导
user.saveLeader(leader);
//领导增加下属
leader.increaseSubordination(user);
}
}
应用服务作为总体协调者,先通过资源库获取到聚合根,然后调用聚合根或者领域服务中的业务方法,最后再次调用资源库保存聚合根。
作用:
/**
* @Author WDYin
**/
@Service
public class UserApplicationServiceImpl implements UserApplicationService {
@Resource
private DomainEventPublisher domainEventPublisher;
@Resource
private UserRepository userRepository;
@Resource
private UserAssembler userAssembler;
@Resource
private UserDomainService userDomainService;
/**
* 用户注册
* @param userAddCommand 注册信息
*/
@Override
public void register(UserAddCommand userAddCommand) {
//业务检查
userDomainService.check(userAddCommand);
//组装user领域模型
User user = userAssembler.commandToDo(userAddCommand);
//落库
userRepository.add(user);
//发送用户注册事件
domainEventPublisher.publish(new UserRegisterEvent(user, UserEventTypeEnum.REGISTER));
}
/**
* 用户修改
* @param userUpdateCommand 修改信息
*/
@Override
public void update(UserUpdateCommand userUpdateCommand) {
User user = userRepository.getById(userUpdateCommand.getId());
//发送用户注册事件
domainEventPublisher.publish(new UserRegisterEvent(user, UserEventTypeEnum.CREATED));
}
/**
* 用户升职
* @param userId 用户id
* @param leaderId 领导的id
*/
@Override
public void promote(UserId userId, UserId leaderId) {
//获取用户领域模型
User user = userRepository.getById(userId.getUserId());
//获取用户的新领导
User leader = userRepository.getById(leaderId.getUserId());
//领域服务升职方法
userDomainService.promote(user,leader);
}
}
负责提供聚合根或者持久化聚合根。仓库帮助我们持久化整个聚合的,存一个对象会把相关对象都存下来。从技术上讲,Repository和DAO所扮演的角色相似,不过DAO的设计初衷只是对数据库的一层很薄的封装,而Repository是更偏向于领域模型。另外,在所有的领域对象中,只有聚合根才“配得上”拥有Repository,而DAO没有这种约束。
代码如下:
/**
* @Author WDYin
* @Date 2022/5/15
**/
@Repository
public class UserRepositoryImpl implements UserRepository {
@Resource
private UserPersistence userPersistence;
@Resource
private UserConverter userConverter;
@Override
public void add(User user) {
UserPO userPo = userConverter.doToPo(user);
userPersistence.insert(userPo);
user.setId(userPo.getId());
}
@Override
public User getById(Long id) {
UserPO userPO = userPersistence.selectByPrimaryKey(id);
return userConverter.poToDo(userPO);
}
}
比如说创建一个实体,里面有五个值对象组成,每次创建的时候都得new一次,这里用工厂简化,工厂帮助我们创建聚合。这一方面可以享受到工厂模式本身的好处,另一方面,DDD中的Factory还具有将“聚合根的创建逻辑”显现出来的效果。Factory有两种实现方式:
1)直接在聚合根中实现Factory方法,常用于简单的创建过程
2)独立的Factory类,用于有一定复杂度的创建过程,或者创建逻辑不适合放在聚合根上
工厂也可以使用converter来代替
代码如下:
属性的拷贝使用BeanUtils或者mapstruct都可以。
@Component
public class UserConverter {
public User poToDo(UserPO userPO) {
User user = new User();
BeanUtils.copyProperties(userPO,user);
return user;
}
public UserPO doToPo(User user) {
UserPo userPO = new UserPo();
BeanUtils.copyProperties(userPO,user);
return userPO;
}
}
当某个功能模块需要依赖第三方系统提供的数据或者功能时,我们常用的策略就是直接使用外部系统的API、数据结构。这样存在的问题就是,因使用外部系统,而被外部系统的质量问题影响,从而“腐化”本身设计的问题。
因此我们的解决方案就是在两个系统之间加入一个中间层,隔离第三方系统的依赖,对第三方系统进行通讯转换和语义隔离,这个中间层,我们叫它防腐层。
说白了就是,两个系统之间加了中间层,中间层类似适配器模式,解决接口差异的对接,接口转换是单向的(即从调用方向被调用方进行接口转换);防腐层强调两个子系统语义解耦,接口转换是双向的。
防腐层作用:
DDD分为战略设计和战术设计。
参与人员:业务专家(领域专家),产品经理,技术专家(研发人员)
战略设计更偏向于软件架构的层面,官方解释,在某个领域,核心围绕上下文的设计。讲求的是领域和限界上下文(Bounded Context,BC)的划分,以及各个限界上下文之间的上下游关系,还有通用语言的设计。也就是从业务视角出发,归好类,把边界划分好,明确界限上下文,可以用事件风暴来做。会得到通用语言,上下文,上下文之间的交互关系,边界,不同的域。
说白了就是,在某个系统,核心围绕子系统的设计;主要关注,这些子系统的划分,子系统的交互方式,还有子系统的核心术语的定义。当前如此火热的“在微服务中使用DDD”这个命题,究其最初的逻辑无外乎是“DDD中的限界上下文可以用于指导微服务中的服务划分”,
事实上,限界上下文依然是软件模块化的一种体现。
三步走:
第一步:需求分析,根据需求划分出初步的领域和限界上下文,以及上下文之间的关系;
第二步:领域分析,进一步分析每个上下文内部,抽取每个子域的领域概念,识别出哪些是实体,哪些是值对象;
第三步:领域建模,对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根;
参与人员:技术专家(研发人员)
战术设计便更偏向于编码实现,官方解释,核心关注上下文中的实体建模,定义值对象,实体等,更偏向开发细节。用领域模型指导设计及编码的实现,以技术为主导。
说白了就是,上下文对应的就是某一个子系统,子系统里代码实现怎么设计,就是战术设计要解决的问题。核心关注某个子系统的代码实现,以面向对象的思维设计类的属性和方法,和设计类图没有什么区别,只是有一些规则而已,就是指导我们划分类。DDD战术设计的目的是使得业务能够从技术中分离并突显出来,让代码直接表达业务的本身。
其中包含了实体,聚合,聚合根,值对象,聚合之间的关系,仓库,工厂,防腐层,充血模型,领域服务,领域事件等概念。
战术层面可以说DDD是一种放大的设计模式。
三步走:
第一步:编写核心业务逻辑,由领域模型驱动软件设计,通过代码来表现该领域模型,在实体和领域服务中实现核心业务逻辑;
第二步:为聚合根设计仓储,并思考实体或值对象的创建方式;
第三步:在工程中实践领域模型,并在实践中检验模型的合理性,倒推模型中不足的地方并重构。
分层架构的一个重要原则是每层只能与位于其下方的层发生耦合,较低层绝不能直接访问较高层。分层架构可以简单分为两种:
严格分层架构:
某层只能与位于其直接下方的层发生耦合
松散分层架构:
则允许某层与它的任意下方层发生耦合
我们在实际运用过程中多使用的是松散分层架构。
将领域模型和业务逻辑分离出来,并减少对基础设施、用户界面甚至应用层逻辑的依赖,因为它们不属业务逻辑。将一个夏杂的系统分为不同的层,每层都应该具有良好的内聚性,并且只依赖于比其自身更低的层。
传统分层架构的 基础设施层 位于底层,持久化和消息机制便位于该层。可将基础设施层中所有组件看作应用程序的低层服务,较高层与该层发生耦合以复用技术基础设施。即便如此,依然应避免核心的领域模型对象与基础设施层直接耦合。
传统架构的缺陷:就是将基础设施层放在最底层存在缺点,比如此时领域层中的一些技术实现令人头疼:违背分层架构的基本原则,难以编写测试用例等。
因此通过Java设计六大原则中的依赖倒置原则实现各层对基础资源的解耦:也就是低层服务(如基础设施层)应依赖高层组件(比如用户界面层、应用层和领域层)所提供接口。高层定义好仓库的接口,基础设施层实现各层定义好的仓库接口。
依赖倒置原则:具体依赖于抽象,而不是抽象依赖于具体。
①一般包括用户接口、Web 服务、rpc请求,mq消息等外部输入均被视为外部输入的请求。对外暴露API,具体形式不限于RPC、Rest API、消息等。
②一般都很薄,提供必要的参数校验和异常捕获流程。
③一般会提供VO或者DTO到Entity或者ValueObject的转换,用于前后端调用的适配,当然dto可以直接使用command和query,视情况而定。
④用户接口层很重要,在于前后端调用的适配。若你的微服务要面向很多应用或渠道提供服务,而每个渠道的入参出参都不一样,你不太可能开发出太多应用服务,这样Facade接口就起很好的作用了,包括DO和DTO对象的组装和转换等。
①应用层方法提供用例级别的能力透出,不处理业务逻辑,而只是调用领域层,对领域服务/聚合根方法调用的封装,负责领域的组合、编排、转发、转换和传递。
②应用服务作为总体协调者,先通过资源库获取到聚合根,然后调用聚合根中的业务方法,最后再次调用资源库保存聚合根。
③除了同步方法调用外,还可以发布或者订阅领域事件,权限校验、事务控制,一个事务对应一个聚合根。
④应用层方法主要执行服务编排等轻量级逻辑,尤其针对跨多个领域的业务场景,效果明显。
⑤参数校验,简单的crud,可直接调用仓库接口
⑥跨上下文(kafka,RocketMq)和上下文内(spring事件,Guava Event Bus)的领域事件
⑦仓储层接口
①包含了业务核心的领域模型:实体(聚合根+值对象),使用充血模型实现所有与之相关的业务功能,主要表达业务概念,业务状态信息以及业务规则。
②真正的业务逻辑都在领域层编写,聚合根负责封装实现业务逻辑,对应用层暴露领域级别的服务接口。
③聚合根不能直接操作其它聚合根,聚合根与聚合根之间只能通过聚合根ID引用;同限界上下文内的聚合之间的领域服务可直接调用;两个限界上下文的交互必须通过应用服务层抽离接口->适配层适配。
④跨实体的状态变化,使用领域服务,领域服务不能直接修改实体的状态,只能调用实体的业务方法
⑤在所有的领域对象中,只有聚合根才拥有Repository,因为Repository不同于DAO,它所扮演的角色只是向领域模型提供聚合根。
⑥防腐层接口
⑦仓储层接口
①为业务逻辑提供支撑能力,提供通用的技术能力,仓库写增删改查类似DAO。
② 防腐层实现(封装变化)用于业务检查和隔离第三方服务,内部try catch
③ 聚合根工厂负责创建聚合根,但并非必须的,可以将聚合根的创建写到聚合根下并改为静态方法。工厂组组装复杂对象,可能会调用第三方服务,仓库集成工厂Facotry/build应对复杂对象的封装,也可以使用converter。
④ 多于技术有关,如:DB交互的接口、Cache相关、MQ、工具类等
⑤抽象系统内第三方组件的交互,为上层提供技术层面的支持,与业务细节无关。
在整洁架构里,同心圆代表应用软件的不同部分,从里到外依次是领域模型、领域服务、应用服务和最外围的容易变化的内容,比如用户界面和基础设施。
整洁架构最主要的原则是依赖原则,它定义了各层的依赖关系,越往里依赖越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的任何情况。
在洋葱架构中,各层的职能划分:
领域模型实现领域内核心业务逻辑,它封装了企业级的业务规则。领域模型的主体是实体,一个实体可以是一个带方法的对象,也可以是一个数据结构和方法集合。
领域服务实现涉及多个实体的复杂业务逻辑。应用服务实现与用户操作相关的服务组合与编排,它包含了应用特有的业务流程规则,封装和实现了系统所有用例。
最外层主要提供适配的能力,适配能力分为主动适配和被动适配。主动适配主要实现外部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。
红圈内的领域模型、领域服务和应用服务一起组成软件核心业务能力。
CQRS — Command Query Responsibility Segregation,故名思义是读写分离,就是将 command 与 query 分离的一种模式。
Command :命令则是对会引起数据发生变化操作的总称,即我们常说的新增,更新,删除这些操作,都是命令。
Query:查询则和字面意思一样,即不会对数据产生变化的操作,只是按照某些条件查找数据。
Command 与 Query 对应的数据源可以公用一种数据源,也可以是互相独立的,即更新操作在一个数据源,而查询操作在另一个数据源上。
CQRS三种模式
(1)共享模型/共享存储:读写公用一种领域模型,读写模型公用一种。
(2)分离模型/共享存储:读写分别用不同的领域模型,读操作使用读领域模型,写操作使用写领域模型。
(3)分离模式/分离存储:也叫做事件源 (Event source) CQRS,使用领域事件保证读写数据的一致性。也就是当 command 系统完成数据更新的操作后,会通过领域事件的方式通知 query 系统。query 系统在接受到事件之后更新自己的数据源。
CQRS(读写操作分别使用不同的数据库)
软件中的读模型和写模型是很不一样的,我们通常所讲的业务逻辑更多的时候是在写操作过程中需要关注的东西,而读操作更多关注的是如何向客户方返回恰当的数据展现。
因此在DDD的写操作中,我们需要严格地按照“应用服务 -> 聚合根 -> 资源库”的结构进行编码,而在读操作中,采用与写操作相同的结构有时不但得不到好处,反而使整个过程变得冗繁,还多了模型转换,影响效率。本来读操作就需要速度快,性能高。
因此本文CQRS实战中的读操作是基于数据模型,应用层提供一个单独的用于读的仓库,然后绕过聚合根和资源库,也就是绕过领域层,在应用层直接返回数据。而写操作是基于领域模型,通过应用服务->聚合根/领域服务->资源库的代码结构进行编码。
六边形架构的核心理念是:应用是通过端口与外部进行交互的
下图的红圈内的核心业务逻辑(应用程序和领域模型)与外部资源(包括 APP、Web 应用以及数据库资源等)完全隔离,仅通过适配器进行交互。它解决了业务逻辑与用户界面的代码交错问题,很好地实现了前后端分离。六边形架构各层的依赖关系与整洁架构一样,都是由外向内依赖。
六边形架构将系统分为内六边形和外六边形两层,这两层的职能划分如下:红圈内的六边形实现应用的核心业务逻辑;外六边形完成外部应用、驱动和基础资源等的交互和访问,对前端应用以 API 主动适配的方式提供服务,对基础资源以依赖倒置被动适配的方式实现资源访问。六边形架构的一个端口可能对应多个外部系统,不同的外部系统也可能会使用不同的适配器,由适配器负责协议转换。这就使得应用程序能够以一致的方式被用户、程序、自动化测试和批处理脚本使用。
这三种架构模型的设计思想微服务架构高内聚低耦合原则的完美体现,而它们身上闪耀的正是以领域模型为中心的设计思想,将核心业务逻辑与外部应用、基础资源进行隔离。
红色框内部主要实现核心业务逻辑,但核心业务逻辑也是有差异的,有的业务逻辑属于领域模型的能力,有的则属于面向用户的用例和流程编排能力。按照这种功能的差异,我们在这三种架构中划分了应用层和领域层,来承担不同的业务逻辑。
领域层实现面向领域模型,实现领域模型的核心业务逻辑,属于原子模型,它需要保持领域模型和业务逻辑的稳定,对外提供稳定的细粒度的领域服务,所以它处于架构的核心位置。
应用层实现面向用户操作相关的用例和流程,对外提供粗粒度的 API 服务。它就像一个齿轮一样进行前台应用和领域层的适配,接收前台需求,随时做出响应和调整,尽量避免将前台需求传导到领域层。应用层作为配速齿轮则位于前台应用和领域层之间。
CQRS(Command Query Responsibility Segregation)是将Command(命令)与Query(查询)分离的一种模式。其基本思想在于:任何一个方法都可以拆分为命令和查询两部分:
Command:不返回任何结果(void),但会改变对象的状态。Command是引起数据变化操作的总称,一般会执行某个动作,如:新增,更新,删除等操作。操作都封装在Command中,用户提交Commond到CommandBus,然后分发到对应的CommandHandler中执行。Command执行后通过Repository将数据持久化。事件源(Event source)CQRS,Command将特定的Event发送到EventBus,然后由特定的EventHandler处理。
Query:返回查询结果,不会对数据产生变化的操作,只是按照某些条件查找数据。基于Query条件,返回查询结果;为不同的场景定制不同的Facade。
基于四层的CQRS架构图:
第一种是
第二种是:
每一层都有自己特定的数据,可以做如下区分:
模型转换架构图:
│
│ ├─interface 用户接口层
│ │ └─controller 控制器,对外提供(Restful)接口
│ │ └─facade 外观模式,对外提供本地接口和dubbo接口
│ │ └─mq mq消息,消费者消费外部mq消息
│ │
│ ├─application 应用层
│ │ ├─assembler 装配器
│ │ ├─dto 数据传输对象,xxxCommand/xxxQuery/xxxVo
│ │ │ ├─command 接受增删改的参数
│ │ │ ├─query 接受查询的参数
│ │ │ ├─vo 返回给前端的vo对象
│ │ ├─service 应用服务,负责领域的组合、编排、转发、转换和传递
│ │ ├─repository 查询数据的仓库接口
│ │ ├─listener 事件监听定义
│ │
│ ├─domain 领域层
│ │ ├─entity 领域实体
│ │ ├─valueobject 领域值对象
│ │ ├─service 领域服务
│ │ ├─repository 仓库接口,增删改的接口
│ │ ├─acl 防腐层接口
│ │ ├─event 领域事件
│ │
│ ├─infrastructure 基础设施层
│ │ ├─converter 实体转换器
│ │ ├─repository 仓库
│ │ │ ├─impl 仓库实现
│ │ │ ├─mapper mybatis mapper接口
│ │ │ ├─po 数据库orm数据对象
│ │ ├─ack 实体转换器
│ │ ├─mq mq消息
│ │ ├─cache 缓存
│ │ ├─util 工具类
│ │
│
如果看到这里,说明你喜欢这篇文章,请转发,点赞。关注【微信公众号】微信搜索【老板来一杯java】回复【进群】即可进入无广告交流群!回复【java】即可获取【java基础经典面试】一份!加群后即可获取【DDD领域驱动设计实战落地解惑】PDF一份!
推荐好文:
使用高并发利器redis—解决淘宝/微博的【热门搜索】和【最近搜索】的功能
SpringCloud核心组件概述(五大神兽)
IntelliJ idea搭建微服务spring cloud框架(一)
MySQL查询语句执行顺序以及各关键字的详解