DDD之Repository模式
一,传统架构下的实体模型
传统的应用架构,几乎就是根据需求设计数据库的表,根据表建立实体,对应着实体的就是DAO,Service,Controller,也就是传统的MVC三层架构。
回顾下我们平时写的代码,里面有着很多的xxxUtils工具类,很多的参数校验逻辑与业务逻辑混杂在一起,很多的实体类直接与数据库进行一对一映射。
好处很明显,在业务初期,开发起来很容易,相对比较简单,流水线式编码,但是,一旦后期需求变更,业务改造,数据库表发生变化,可能给我们带来毁灭性的负担。所谓牵一发而动全身,前面欠下了技术债,后面很难受,想补救工作量巨大,不补救,系统难以升级,难以扩展,灵活性急剧下降。
对于第三方(包括但不限于数据库)的强依赖,导致我们在做业务扩展的时候,顾虑重重,缺少了一往无前的动力。
但是截至目前,包括我所参与开发的项目,依然是采用这种模式,为什么?
- 数据库思维:从有了数据库的那一天起,开发人员的思考方式就逐渐从“写业务逻辑“转变为了”写数据库逻辑”,也就是我们经常说的在写CRUD代码。
- 所谓简单:贫血模型的优势在于“简单”,仅仅是对数据库表的字段映射,所以可以从前到后用统一格式串通。这里简单打了引号,是因为它只是表面上的简单,实际上当未来有模型变更时,你会发现其实并不简单,每次变更都是非常复杂的事情。
- 脚本思维:很多常见的代码都属于“脚本”或“胶水代码”,也就是流程式代码。脚本代码的好处就是比较容易理解,但长久来看缺乏健壮性,维护成本会越来越高。
两个概念,你是否明确?
- 数据模型:也就是和数据库一一映射的类
- 业务模型/领域模型:业务逻辑中,相关连的数据如何联动
真实代码结构中,Data Model和 Domain Model实际上会分别在不同的层里,Data Model只存在于数据层,而Domain Model在领域层,而链接了这两层的关键对象,就是Repository。
二,Repository的作用
在传统的MVC三层架构中,我们操作数据库的层,一般叫做DAO,或者Mapper层。
由于他与数据库直接耦合,导致了强依赖性。更可怕的是,由于我们都是在Service层直接注入Mapper层,导致了这种强依赖的传递,也就是整个应用体系开始变得越加依赖数据库DB。
举一个例子
public Interface UserDao{public List<User> selectUserByIds(List<Integer> ids);
}public class UserService{@Resourceprivate UserDao userDao;public List<User> getUserList(List<Integer> ids){return userDao.selectUserByIds(ids);}}
这个代码,咋一看,简单明了,没有任何问题。但是假如现在由于数据量的增长和访问数量的增加,我需要引入缓存的逻辑,假如有十个地方调用了这个DAO中的方法,我需要在这十个地方都修改成:
public class UserService{@Resourceprivate UserDao userDao;@Resourceprivate RedisTemplate redisTemplate;public List<User> getUserList(List<Integer> ids){List<User> users=redisTemplate.opsForValue().get(key);if(users!=null){return users;}else{List<User> userList=userDao.selectUserByIds(ids);redisTemplate.opsForValue().set(key,userList);return userList;}}}
所以,需要一个逻辑,能够隔离业务逻辑与DB之间的传递强耦合关系,让我们的应用更加灵活,健壮,这个就是Repository的价值。
三,模型对象代码规范
1.什么是DO,DTO,Entity?
- Data Object:DO的字段类型和名称应该和数据库物理表格的字段类型和名称一一对应,这样我们不需要去跑到数据库上去查一个字段的类型和名称。
- Entity:实体对象是我们正常业务应该用的业务模型,它的字段和方法应该和业务语言保持一致,和持久化方式无关。也就是说,Entity和DO很可能有着完全不一样的字段命名和字段类型,甚至嵌套关系。Entity的生命周期应该仅存在于内存中,不需要可序列化和可持久化。
- DTO:主要作为Application层的入参和出参,比如CQRS里的Command、Query、Event,以及Request、Response等都属于DTO的范畴。DTO的价值在于适配不同的业务场景的入参和出参,避免让业务对象变成一个万能大对象。
2.对象之间的关系
在实际开发中DO、Entity和DTO不一定是1:1:1的关系。一些常见的非1:1关系如下:
复杂的实体拆分成多张数据库的表:常见的原因,字段多,查询性能差,需要将非检索、大字段等单独存为一张表,提升基础信息表的检索效率。
当然,除了一些数据查询频繁,聚合性非常强的表。
拆分的实体:我接触过的,订单,商品,购物车。
3.模型所在模块和转化器
由于现在从一个对象变为3+个对象,对象间需要通过转化器(Converter/Mapper)来互相转化。而这三种对象在代码中所在的位置也不一样,简单总结如下:
DTO Assembler:在Application层,Entity到DTO的转化器有一个标准的名称叫DTO Assembler。Martin Fowler在P of EAA一书里对于DTO 和 Assembler的描述:Data Transfer Object。DTO Assembler的核心作用就是将1个或多个相关联的Entity转化为1个或多个DTO。
Data Converter:在Infrastructure层,Entity到DO的转化器没有一个标准名称,但是为了区分Data Mapper,我们叫这种转化器Data Converter。这里要注意Data Mapper通常情况下指的是DAO,比如Mybatis的Mapper。Data Mapper的出处也在P of EAA一书里:Data Mapper
如果是手写一个Assembler,通常我们会去实现2种类型的方法,如下;Data Converter的逻辑和此类似,略过。
public class DtoAssembler { // 通过各种实体,生成DTO public OrderDTO toDTO(Order order, Item item) { OrderDTO dto = new OrderDTO(); dto.setId(order.getId()); dto.setItemTitle(item.getTitle()); // 从多个对象里取值,且字段名称不一样 dto.setDetailAddress(order.getAddress.getDetail());// 可以读取复杂嵌套字段 // 省略N行 return dto; }// 通过DTO,生成实体 public Item toEntity(ItemDTO itemDTO) { Item entity = new Item(); entity.setId(itemDTO.getId()); // 省略N行 return entity; }
}
我们能看出来通过抽象出一个Assembler/Converter对象,我们能把复杂的转化逻辑都收敛到一个对象中,并且可以很好的单元测试。这个也很好的收敛了常见代码里的转化逻辑。
在调用方使用时是非常方便的:
public class Application { private DtoAssembler assembler; private OrderRepository orderRepository; private ItemRepository itemRepository;public OrderDTO getOrderDetail(Long orderId) { Order order = orderRepository.find(orderId); Item item = itemRepository.find(order.getItemId()); return assembler.toDTO(order, item); // 原来的很多复杂转化逻辑都收敛到一行代码了 }
}
4.模型规范总结
从使用复杂度角度来看,区分了DO、Entity、DTO带来了代码量的膨胀(从1个变成了3+2+N个)。但是在实际复杂业务场景下,通过功能来区分模型带来的价值是功能性的单一和可测试、可预期,最终反而是逻辑复杂性的降低。
四,Repository代码规范
1.接口规范
接口名称不应该使用底层实现的语法:我们常见的insert、select、update、delete都属于SQL语法,使用这几个词相当于和DB底层实现做了绑定。相反,我们应该把 Repository 当成一个中性的类 似Collection 的接口,使用语法如 find、save、remove。在这里特别需要指出的是区分 insert/add 和 update 本身也是一种和底层强绑定的逻辑,一些储存如缓存实际上不存在insert和update的差异,在这个 case 里,使用中性的 save 接口,然后在具体实现上根据情况调用 DAO 的 insert 或 update 接口。
出参入参不应该使用底层数据格式:需要记得的是 Repository 操作的是 Entity 对象(实际上应该是Aggregate Root),而不应该直接操作底层的 DO 。更近一步,Repository 接口实际上应该存在于Domain层,根本看不到 DO 的实现。这个也是为了避免底层实现逻辑渗透到业务代码中的强保障。
应该避免所谓的“通用”Repository模式:很多 ORM 框架都提供一个“通用”的Repository接口,然后框架通过注解自动实现接口,比较典型的例子是Spring Data、Entity Framework等,这种框架的好处是在简单场景下很容易通过配置实现,但是坏处是基本上无扩展的可能性(比如加定制缓存逻辑),在未来有可能还是会被推翻重做。当然,这里避免通用不代表不能有基础接口和通用的帮助类。
先定义一个基础的 Repository 基础接口类,以及一些Marker接口类:
public interface Repository<T extends Aggregate<ID>, ID extends Identifier> {/** * 将一个Aggregate附属到一个Repository,让它变为可追踪。 * Change-Tracking在下文会讲,非必须 */ void attach(@NotNull T aggregate);/** * 解除一个Aggregate的追踪 * Change-Tracking在下文会讲,非必须 */ void detach(@NotNull T aggregate);/** * 通过ID寻找Aggregate。 * 找到的Aggregate自动是可追踪的 */ T find(@NotNull ID id);/** * 将一个Aggregate从Repository移除 * 操作后的aggregate对象自动取消追踪 */ void remove(@NotNull T aggregate);/** * 保存一个Aggregate * 保存后自动重置追踪条件 */ void save(@NotNull T aggregate);
}
// 聚合根的Marker接口
public interface Aggregate<ID extends Identifier> extends Entity<ID> {}
// 实体类的Marker接口
public interface Entity<ID extends Identifier> extends Identifiable<ID> {}
public interface Identifiable<ID extends Identifier> { ID getId();
}
// ID类型DP的Marker接口
public interface Identifier extends Serializable {}
业务自己的接口只需要在基础接口上进行扩展,举个订单的例子:
// 代码在Domain层
public interface OrderRepository extends Repository<Order, OrderId> {// 自定义Count接口,在这里OrderQuery是一个自定义的DTO Long count(OrderQuery query);// 自定义分页查询接口 Page<Order> query(OrderQuery query);// 自定义有多个条件的查询接口 Order findInStore(OrderId id, StoreId storeId);
}
每个业务需要根据自己的业务场景来定义各种查询逻辑。
这里需要再次强调的是Repository的接口是在Domain层,但是实现类是在Infrastructure层。
2.Repository基础实现
先举个Repository的最简单实现的例子。注意OrderRepositoryImpl在Infrastructure层:
// 代码在Infrastructure层
@Repository // Spring的注解
public class OrderRepositoryImpl implements OrderRepository { private final OrderDAO dao; // 具体的DAO接口 private final OrderDataConverter converter; // 转化器public OrderRepositoryImpl(OrderDAO dao) { this.dao = dao; this.converter = OrderDataConverter.INSTANCE; }@Override public Order find(OrderId orderId) { OrderDO orderDO = dao.findById(orderId.getValue()); return converter.fromData(orderDO); }@Override public void remove(Order aggregate) { OrderDO orderDO = converter.toData(aggregate); dao.delete(orderDO); }@Override public void save(Order aggregate) { if (aggregate.getId() != null && aggregate.getId().getValue() > 0) { // update OrderDO orderDO = converter.toData(aggregate); dao.update(orderDO); } else { // insert OrderDO orderDO = converter.toData(aggregate); dao.insert(orderDO); aggregate.setId(converter.fromData(orderDO).getId()); } }@Override public Page<Order> query(OrderQuery query) { List<OrderDO> orderDOS = dao.queryPaged(query); long count = dao.count(query); List<Order> result = orderDOS.stream().map(converter::fromData).collect(Collectors.toList()); return Page.with(result, query, count); }@Override public Order findInStore(OrderId id, StoreId storeId) { OrderDO orderDO = dao.findInStore(id.getValue(), storeId.getValue()); return converter.fromData(orderDO); }
}
从上面的实现能看出来一些套路:所有的Entity/Aggregate会被转化为DO,然后根据业务场景,调用相应的DAO方法进行操作,事后如果需要则把DO转换回Entity。代码基本很简单,唯一需要注意的是save方法,需要根据Aggregate的ID是否存在且大于0来判断一个Aggregate是否需要更新还是插入。
3.Repository复杂实现
针对单一Entity的Repository实现一般比较简单,但是当涉及到多Entity的Aggregate Root时,就会比较麻烦,最主要的原因是在一次操作中,并不是所有Aggregate里的Entity都需要变更,但是如果用简单的写法,会导致大量的无用DB操作。
举一个常见的例子,在主子订单的场景下,一个主订单Order会包含多个子订单LineItem,假设有个改某个子订单价格的操作,会同时改变主订单价格,但是对其他子订单无影响:
如果用一个非常naive的实现来完成,会导致多出来两个无用的更新操作,如下:
public class OrderRepositoryImpl extends implements OrderRepository { private OrderDAO orderDAO; private LineItemDAO lineItemDAO; private OrderDataConverter orderConverter; private LineItemDataConverter lineItemConverter;// 其他逻辑省略@Override public void save(Order aggregate) { if (aggregate.getId() != null && aggregate.getId().getValue() > 0) { // 每次都将Order和所有LineItem全量更新 OrderDO orderDO = orderConverter.toData(aggregate); orderDAO.update(orderDO); for (LineItem lineItem: aggregate.getLineItems()) { save(lineItem); } } else { // 插入逻辑省略 } }private void save(LineItem lineItem) { if (lineItem.getId() != null && lineItem.getId().getValue() > 0) { LineItemDO lineItemDO = lineItemConverter.toData(lineItem); lineItemDAO.update(lineItemDO); } else { LineItemDO lineItemDO = lineItemConverter.toData(lineItem); lineItemDAO.insert(lineItemDO); lineItem.setId(lineItemConverter.fromData(lineItemDO).getId()); } }
}
在这个情况下,会导致4个UPDATE操作,但实际上只需要2个。在绝大部分情况下,这个成本不高,可以接受,但是在极端情况下(当非Aggregate Root的Entity非常多时),会导致大量的无用写操作。
五,Repository迁移路径
在我们日常的代码中,使用Repository模式是一个很简单,但是又能得到很多收益的事情。最大的收益就是可以彻底和底层实现解耦,让上层业务可以快速自发展。
我们假设现有的传统代码包含了以下几个类(还是用订单举例):
- OrderDO
- OrderDAO
可以通过以下几个步骤逐渐的实现Repository模式:
生成Order实体类,初期字段可以和OrderDO保持一致
生成OrderDataConverter,通过MapStruct基本上2行代码就能完成
写单元测试,确保Order和OrderDO之间的转化100%正确
生成OrderRepository接口和实现,通过单测确保OrderRepository的正确性
将原有代码里使用了OrderDO的地方改为Order
将原有代码里使用了OrderDAO的地方都改为用OrderRepository
通过单测确保业务逻辑的一致性。
DDD之Repository模式相关推荐
- MVC架构中的Repository模式 个人理解
关于MVC架构中的Repository模式 个人理解:Repository是一个独立的层,介于领域层与数据映射层(数据访问层)之间.它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提 ...
- java repository模式_MVC架构中的Repository模式 个人理解
个人理解:Repository是一个独立的层,介于领域层与数据映射层(数据访问层)之间.它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问.Reposit ...
- 分享基于EF6、Unitwork、Autofac的Repository模式设计
目录 分享基于EF6.Unitwork.Autofac的Repository模式设计 一.实现的思路和结构图 二.Repository设计具体的实现代码 三.Repository设计的具体的使用 四. ...
- 分享基于Entity Framework的Repository模式设计(附源码)
关于Repository模式,在这篇文章中有介绍,Entity Framework返回IEnumerable还是IQueryable? 这篇文章介绍的是使用Entity Framework实现的Rep ...
- Repository模式
近来发现很多ASP.NET MVC的例子中都使用了Repository模式,比如Oxite,ScottGu最近发布的免费的ASP.NET MVC教程都使用了该模式.就简单看了下. 在<企业架构模 ...
- Repository模式与UnitOfWorks模式的运用
软件开发就像是一个江湖,而设计模式就是一本高深的秘籍每读一次.用一次.想一次都能得到新的领悟,让我们的设计技能有所提高.开始时我们可能会"为了模式而模式",让代码变得乱78糟甚至难 ...
- 基于NHibernate的UnitOfWork+Repository模式(AutoFac)–Part2
非常感谢各位在之前comment中提出的意见..确实Razor语法介绍的文章错别字太多,影响大家阅读. 前面的文章介绍了如何在asp.net中中UnitOfWork模式以及Repository模式的使 ...
- Repository模式(转载)
近来发现很多ASP.NET MVC的例子中都使用了Repository模式,比如Oxite,ScottGu最近发布的免费的ASP.NET MVC教程都使用了该模式.就简单看了下. 在<企业架构模 ...
- MVC+LINQToSQL的Repository模式之(二)数据基类
namespace Data.TEST { /// <summary> /// 数据操作基类 /// </summary> public abs ...
- 从壹开始微服务 [ DDD ] 之一 ║ D3模式设计初探 与 我的计划书
缘起 哈喽大家周四好!又是开心的一天,时间过的真快,我们的 <从壹开始 .net core 2.1 + vue 2.5 >前后端分离系列共 34 篇已经完结了,当然以后肯定还会有更新和修改 ...
最新文章
- chapter15 机器学习之大数据与mapreduce
- 当null检查非常失败时
- 文献阅读6-Entity-Relation Extraction as Multi-turn Question Answering(实体关系联合抽取,层次标签依赖关系,multi-turn QA)
- 骂人的代码_楚河骚话不断粉丝求代码 罕见喷脏怒怼弹幕
- 第2次预习课-0704
- 今天你写控件了吗?----ASP.net控件开发系列(八)
- ubuntu安装vsftpd遇到的问题
- 快速数论变换(NTT)
- Typora 博文标题自动编号
- 通过Windows组策略限制证书组织流氓软件的安装运行
- 计算机系统组成导学案,单元一任务2认识计算机系统的组成导学案.pdf
- carmaker/matlab联合仿真(二) 新建测试场景
- 实训流水账之day01—安装软件
- Matlab的循环语法
- 动态路由协议:华为路由器配置RIP协议
- 在桌面为计算机程序创建快捷方式,什么是快捷方式,电脑快捷方式有什么用?...
- GraphSAGE+FM+Transformer强强联手:评微信的GraphTR模型
- PKPM学习:关于“悬空柱”的问题
- Havel-Hakimi定理问题
- win10系统cmd窗口设置定时自动关机及取消命令
热门文章
- 展览 | 2018届中国国际信息通信展览的所见所闻
- 【 数理逻辑 二 】逻辑概述、现代逻辑学发展历程和形式系统与形式语言
- java引入math包_JAVA math包
- Java中的Math函数常用方法总结
- HTML5+CSS3笔记 (黑马pink老师)
- java中的g1_G1GC 概念与性能调优
- 计算机二级考试加油作文,为中考而加油作文(精选10篇)
- MATCH和INDEX函数
- The name `AssetDatabase' does not exist in the current context
- android h5 qq登录,Android webview一键登录手机QQ(2018.11)