干货来袭!详述DDD领域驱动设计之领域接口化设计
zhezhongyun 2025-07-24 23:18 33 浏览
- 领域接口化设计
- 领域接口化
- 关联接口化
- 系统接口化
- 开源电商
- 总结
领域接口化设计
把服务对象(service)和资源库对象(repository)设计成接口是最常见的。但是这对接口化的认识还远远不够,我们需要更深入地去分析接口化设计和更全面地应用接口化编程。所以我们要讨论的是全面接口化,尤其是对领域的模型 接口化的认识。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
领域接口化
通常的情况下我们会把领域模型设计成类(class) ,但是你有没有想过把领域模型设计成接口(interface) ?比如:
public interface User {
// ...
}
public class UserImpl implements User {
// ...
}
这样的设计似乎没有任何价值,那么继续深入地看看。比如:
user-object-uml
这时候看起来有点东西,因为我们为了适配不同的数据源 ,提供了不同的实现类。
最开始要把领域对象 设计成接口,确实是为了在不同的 ORM 框架之间实现无缝切换 。因为 JPA 对面向对象的支持最好,而 Mybatis 因为简单在大环境下比较流行。在解决这个问题时,通常使用层内包裹 或者叫对象转换 的方式来解决。具体来说是在持久层使用持久化对象(PO)与领域对象(DO)的之间进行转换。例如:
public class JpaUserRepository implements UserRepository {
// ...
@Override
public Optional<User> findById(String id) {
UserPO userPO = this.entityManager.find(UserPO.class, id);
return Optional.ofNullable(userPO).map(UserPO::toUser);
}
@Override
public User save(User user) {
UserPO userPO = this.entityManager.find(UserPO.class, user.getId());
userPO.setNickname(user.getNickname());
// ...
return this.entityManager.merge(userPO).toUser();
}
}
其中 UserPO 对象基本上是对数据库表的映射,然后将数据与 User 对象进行交换。对于这种需要交换的方式既有性能的损失又比较繁琐,将 User 设计成接口后,这个交换的问题就比较简单地解决了,如下:
public class JpaUserRepository implements UserRepository {
// ...
@Override
public User create(String id) {
return new JpaUser(id);
}
@Override
public Optional<User> findById(String id) {
JpaUser user = this.entityManager.find(JpaUser.class, id);
return Optional.ofNullable(user);
}
@Override
public User save(User user) {
JpaUser target = JpaUser.of(user);
return this.entityManager.merge(target);
}
// ...
}
补充 JpaUser.of() 方法的实现:
public class JpaUser extends UserSupport {
// ...
public static JpaUser of(User user) {
if (user instanceof JpaUser) {
return (JpaUser) user;
}
var target = new JpaUser();
BeanUtils.copyProperties(user, target);
// ...
return target;
}
}
对于使用 JPA 或者 Elasticsearch 等等各种不同的数据源,Spring data 都为此做了全面的支持。但由于 User 是接口,Spring data 提供的 Repository 接口泛型 只支持具体类型 ,比如:
public interface ElasticsearchUserRepository
extends ElasticsearchRepository<ElasticsearchUser, String> {
// extends ElasticsearchRepository<User, String> // Not supported
}
为了解决这个问题,我们需要使用委托的方式,如下:
public class DelegatingElasticsearchUserRepository implements UserRepository {
private final ElasticsearchUserRepository elasticsearchUserRepository;
public DelegatingElasticsearchUserRepository(ElasticsearchUserRepository elasticsearchUserRepository) {
this.elasticsearchUserRepository = elasticsearchUserRepository;
}
@Override
public User create(String id) {
return new ElasticsearchUser(id);
}
@Override
public Optional<User> findById(String id) {
return CastUtils.cast(this.elasticsearchUserRepository.findById(id));
}
@Override
public User save(User user) {
return this.elasticsearchUserRepository.save(ElasticsearchUser.of(user));
}
// ...
}
基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。
项目地址:https://github.com/YunaiV/onemall
关联接口化
order-association
接口之间的关联关系依然需要具体到子类的关联关系上来讨论。
对于需要持久化的实体来说,我们不可能直接在成员属性上使用接口类型,因为持久化框架无法通过接口来判定具体实现类。如下:
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "mf_order")
public class JpaOrder implements Order {
// ...
// OrderItem 是一个接口类型,不能持久化。
private List<OrderItem> items = new ArrayList<>();
// ...
}
对于泛化 关联关系问题,我们可以使用 JPA 注解提供的 targetEntity 属性来解决:
// ...
public class JpaOrder implements Order {
// ...
// 通过指定具体的 targetEntity 类型,来解决泛化与特化的问题。
@OneToMany(targetEntity = JpaOrderItem.class)
private List<OrderItem> items = new ArrayList<>();
// ...
}
- 支持 targetEntity 属性的注解包括:@OneToMany、@OneToOne、@ManyToOne、@ManyToMany。
对于不支持类似 targetEntity 属性的框架或者其它持久化技术,我们可以使用封装 来解决。如下:
@Getter
@Setter
@NoArgsConstructor
@Document(indexName = "user")
public class ElasticsearchOrder implements Order {
// ...
// 使用具体特化类型进行解决。
private List<ElasticsearchOrderItem> items = new ArrayList<>();
@Override
public void setItems(List<OrderItem> items) {
this.items = Objects.requireNonNullElseGet(items, (Supplier<List<OrderItem>>) ArrayList::new)
.stream().map(ElasticsearchOrderItem::of).collect(Collectors.toList());
}
// ...
}
如果使用的是 Mybatis 作为持久化框架,依然可以在 OrderMapper.xml 中进行配置来解决:
<resultMap id="Order" type="org.mallfoundry.order.repository.mybatis.MybatisOrder">
<!-- ... -->
<collection property="items" ofType="org.mallfoundry.order.repository.mybatis.MybatisOrderItem">
<!-- ... -->
</collection>
<!-- ... -->
</resultMap>
在解决掉不同数据源无缝切换和关联关系特化的问题后,在创建 User 对象上就和以往使用 new 的方式有所不同了,如下:
@Test
public void testCreateUser() {
User user = this.userService.createUser(null); // new User()
user.setNickname("Nickname");
user.setGender(Gender.MALE);
this.userService.addUser(user);
}
在过去创建对象都是使用 new 关键字,然而现在要使用 UserService 提供的 createUser(String id) 来创建。
这种思维的转变可能让你初次不太很适应,但在考虑另一个问题。
系统接口化
对于一个产品我们要考虑的不只是产品本身能解决的业务需求,还需要在部署上有所追求。如果项目初期的并发量很小,客户可能采用单进程的方式部署,慢慢地单进程扛不住了会升级到集群的方式,最终还要升级到微服务的方式。如何在单进程、集群和微服务之间进行无缝切换呢?
再过去单机和集群项目与微服务项目是不能兼容的,因为领域模型都是类(class)而不是接口(interface)。具体来说:服务提供者(provider)的 User 对象与服务消费者(Consumer)的 User 对象是不兼容,不兼容将导致在单机项目中使用的是服务提供方的内部 User 对象,而一旦迁移到微服务项目后,需要大量的修改工作。要把以前调用方使用内部 User 对象替换为服务消费者提供的 User 对象。这样的工作也是不可以逆的,一旦迁移成功就不能降级到单机环境了。
再过去我们确实把服务(service)设计成了接口,这种接口的设计对于内部的开发看似会有帮助,但是从实战的经验来看却不像大家想象的那样可以为 Service 提供不同的实现。因为现在都是迭代开发,都是一个版本一个版本的去不断完善应用服务代码,而不是替换应用服务代码,所以在 IDDD 中把应用服务(Application Service)类型由接口(Interface)改为了类(Class)。
如果我们把领域对象设计成接口类型,并与服务接口以及其它接口一起组织在一个新的模块内,形成一个新的接口(API)模块。然后为各种不同地端口提供适配此端口的实现,这样的设计是不是可以解决在运行环境中无缝切换的问题,如下:
user-modules
这样的设计使得调用者 只需要使用 User 接口(user-api)开发业务,并且在单进程(Standalone)环境中只需要依赖 user 模块,在微服务环境中只需要依赖 user-openfeign-client 模块,在外部环境中只需要依赖 user-rest-client 模块。调用者通过依赖不同地实现模块 来解决不同环境的无缝切换,并且调用者使用的代码是不需要改变的。
开源电商
Mallfoundry 是一个完全开源的使用 Spring Boot 开发的多商户电商平台。它可以嵌入到已有的 Java 程序中,或者作为服务器、集群、云中的服务运行。
- 领域模型采用领域驱动设计(DDD)、接口化以及面向对象设计。
项目地址:https://gitee.com/mallfoundry/mall
推荐 3 个开源项目:
https://github.com/YunaiV/ruoyi-vue-pro
https://github.com/YunaiV/SpringBoot-Labs
https://github.com/YunaiV/onemall
总结
领域对象接口化使得我们在内部实现了一套统一的接口,并将领域对象接口化扩展到系统级别时,我们又在系统层次上设计出一套统一地全局接口来开发业务和应对未来变化的环境。这样的设计虽然非常好,但对软件设计人员、软件架构师以及开发人员的专业性也有了一定的要求,但是它所带来的好处是可见的。
来源:
juejin.cn/post/6894109393173315597
相关推荐
- Python入门学习记录之一:变量_python怎么用变量
-
写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...
- python变量命名规则——来自小白的总结
-
python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...
- Python入门学习教程:第 2 章 变量与数据类型
-
2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...
- 绘制学术论文中的“三线表”具体指导
-
在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...
- Python基础语法知识--变量和数据类型
-
学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...
- 一文搞懂 Python 中的所有标点符号
-
反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...
- Python变量类型和运算符_python中变量的含义
-
别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...
- 从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序
-
在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...
- Python中下划线 ‘_’ 的用法,你知道几种
-
Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...
- 解锁Shell编程:变量_shell $变量
-
引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...
- 一文学会Python的变量命名规则!_python的变量命名有哪些要求
-
目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...
- 更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for
-
src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...
- C++第五课:变量的命名规则_c++中变量的命名规则
-
变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....
- Rust编程-核心篇-不安全编程_rust安全性
-
Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...
- 探秘 Python 内存管理:背后的神奇机制
-
在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- opacity 属性 (32)
- transition 属性 (33)
- 1-1. 变量声明 (31)
