浴室沉思:聊聊DAL和Repository
这是一个由DDD群引发的随笔
在写了上一篇随笔《关于ORM的浴室沉思》后一些朋友私聊我,很多刚接触DDD的朋友会对Repository(仓储层)这东西有点疑惑,为什么要叫仓储层?是不是三层的DAL换个名字而已?毕竟大家都是对数据库的操作嘛,而且我用EF还有必要用仓储么?是不是可以省掉这一层?ABP框架的持久化究竟有什么问题?
在回答这些问题之前,我们要先了解一些模型分层的概念。
DO
几乎每位程序员在刚入门时都以三层架构(3-tier architecture)为基础,界面层(User Interface layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data access layer)首次让我们在架构级别了解高内聚低耦合的概念。其中对数据访问层也即是DAL的定义是——屏蔽对“数据文件”的操作,这里的数据文件指的是数据库以及其它持久化实现。
扩展阅读:持久化不一定是数据库,直接写个excel甚至txt也算持久化。例如rabbitmq或者redis就是通过对文件的读写来实现。追根溯源,其实数据库也是如此,例如SQL server的mdf文件。
在最简单的三层中,我们一般有一个models层或者entities层贯通三层用于数据的传递(为了便于描述后文统一叫entities层)。在界面层,entity用于显示数据或者作为view object的转换源;在业务逻辑层,和业务代码组合起来完成业务逻辑的实现;在数据访问层则作为被持久化的对象保存到数据库里。
不管怎么分,即便是后来的MVC,在很长一段时间的绝大部分项目里其实都以这种逻辑来分层,但到了DDD中就遇到了问题,因为DDD的核心模型是DO,而且DO是充血模型。
扩展阅读:所谓充血模型,实际上就是包含了业务逻辑的对象。回想一下面向对象的定义,对象是包含“状态”(属性)以及“行为”(方法/函数)的,其实这很容易理解——贫血模型实际上就是数据的容器,它不是“活的”对象,在任何地方都可以对其进行修改,这实际上违反了高内聚的原则。打个比方:我们现在有个程序员的对象,他要减肥,那么我们要通过调用“健身”这个方法将其Weight属性逐渐降下来,而不是直接在外面set他的体重。
贫血模型大行其道有其历史原因,最早的EJB就是将业务对象分为属性和业务方法两部分,然后spring延续了下来,但spring他爹也说:这实际上是一种面向过程的做法。在这里我们暂时不展开充血模型和贫血模型的讨论。
在DDD的模型划分中,最核心的部分就是DO(Domain Object领域对象)。DO是依据不变性划分出来的业务对象,其属性是public get protected set的,只能通过DO本身的方法来进行操作。举个简单的不严谨的例子,例如520网购,你在帮女盆友清空购物车的时候一般生成一个订单对象,其包含了子订单(例如口红、神仙水),我们大概地将DO设计如下:
pubic class Order
{
public string OrderId {public get; protected set;}
public Status Status {public get; protected set;}
public DateTime CreateTime {public get; protected set;} = Datetime.Now;
public DateTime? PayTime {public get; protected set;}
public DateTime? CommitTime {public get; protected set;}
public List<OrderItem> Items {public get; protected set;}
public void Pay(decimal money)
{
if(Items?.Count == 0)
throw new exception ("请选择商品。");
var totalPrice = Items.Sum(item => item.UnitPrice * item Quantity);
if(money < totalPrice)
throw new exception ("余额不足。")
PayTime = DateTime.Now;
Status = OrderStatus.已支付;
}
public void Commit()
{
if(Status != OrderStatus.已支付)
throw new exception ("请先支付。");
CommitTime = DateTime.Now;
Status = OrderStatus.已确认;
}
}
public class OrderItem
{
public Guid Id {public get; protected set;}
public string Sku {public get; protected set;}
public decimal UnitPrice {public get; protected set;}
public int Quantity {public get; protected set;}
}
public enum OrderStatus
{
待支付,
已支付,
已确认,
.....
}
在这个例子中我们可以条件反射般地想到,RDB中有两张表Order和OrderItem,这两张表是一对多的关系。假如我们用的是DAL,那么可能有个OrderDal和OrderItemDal来将数据分别持久化到RDB中,更严谨些的话还会将它们放到一个事务里执行。当然也可以有个泛型的DBHelper将Order和OrderItem分别持久化。
在这里DAL接收的参数是Order和OrderItem,它封装了对RDB的操作,让我们可以用更友好的方式来使用RDB。但这里我们要思考一个问题:所谓持久化,我们要持久化的是什么?
PO(Persistent Object/持久化对象)
实际上我们持久化的并不是DO,而是DO的“状态”。例如你吃饭时被人偷拍,照片会将你吃饭时的样子记录下来,但吃饭这种“行为”本身无法持久化,如上面的Pay和Commit两个方法。另外DO的一个原则是“原子性”,也就是说拿就整个拿,存就整个存,如果是用仓储层来持久化,则只会有一个OrderRepository,将整个Order作为参数丢进去,仓储层内则将Order和OrderItem的状态部分转为OrderPo和OrderItemPo两种贫血对象,并用之持久化。
重点:只有聚合才有仓储
到了这里我们有了一个初步的概念:DAL是对数据文件操作的封装,不管是否实现了泛型与表的转换,它都是以数据文件为中心,在使用的时候其实我们是以一种面向数据库编程的思维来进行操作;仓储层则是反过来为领域层服务,领域层需要什么它才提供什么,屏蔽掉底层持久化的具体实现。
我们已经用了ORM(EF)了,还有必要用仓储层么?
其实还是要的,上一篇随笔《关于ORM的浴室沉思》说明了ORM的概念,但遗憾的是现在所有ORM实际上都没有彻底解决阻抗失配的问题。
另外DO是原子性的,因此只有聚合才有仓储,上述例子OrderItem不属于聚合,它是没有仓储的。假如直接用EF的话代码是可以直接访问到DbContext<OrderItem>,这就对代码造成了隐患。
而且直接使用EF的话,所有的业务代码将会对EF造成高度耦合,这会造成潜在的技术风险。假如我们使用的是仓储层,到时候只需要在仓储层捣鼓就行。实际上DDD项目中,ORM反而不是必要的东西。
所以ABP的问题是?
ABP是一个很好的框架,土牛的技术水平也很高,但不能说ABP就是DDD的标准答案,例如其仓储层实际上并不是服务于DO而是PO。因此有种说法ABP不是DDD而是DDD LITE,这种说法有其原因。另外仓储层严谨地说不应该提供IQueryable,而应该是Command端需要什么的获取方法才提供,否则会无法进行单元测试。
至于Query端,怎么快怎么来,管它呢~
浴室沉思:聊聊DAL和Repository的更多相关文章
- 从Entity Framework的实现方式来看DDD中的repository仓储模式运用
一:最普通的数据库操作 static void Main(string[] args) { using (SchoolDBEntities db = new SchoolDBEntities()) { ...
- DIV遮罩层传值
今天费了很大的劲儿才搞定!下面贴出代码和总结: 1.首先是前台代码: <%@ Page Title="" Language="C#" MasterPage ...
- 重温ASP.NET WebAPI(二)进阶
重温ASP.NET WebAPI(二)进阶 介绍 本文为个人对WebApi的回顾无参考价值. 本文内容: Rest和UnitOfWork 创建WebAPi的流程 IOC-Unity的使用 MEF ...
- [切图仔救赎]炒冷饭--在线手撸vue2响应式原理
--图片来源vue2.6正式版本(代号:超时空要塞)发布时,尤雨溪推送配图. 前言 其实这个冷饭我并不想炒,毕竟vue3马上都要出来.我还在这里炒冷饭,那明显就是搞事情. 起因: 作为切图仔搬砖汪,长 ...
- Asp.Net Core + Dapper + Repository 模式 + TDD 学习笔记
0x00 前言 之前一直使用的是 EF ,做了一个简单的小项目后发现 EF 的表现并不是很好,就比如联表查询,因为现在的 EF Core 也没有啥好用的分析工具,所以也不知道该怎么写 Linq 生成出 ...
- 初探领域驱动设计(2)Repository在DDD中的应用
概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...
- Repository 仓储,你的归宿究竟在哪?(三)-SELECT 某某某。。。
写在前面 首先,本篇博文主要包含两个主题: 领域服务中使用仓储 SELECT 某某某(有点晕?请看下面.) 上一篇:Repository 仓储,你的归宿究竟在哪?(二)-这样的应用层代码,你能接受吗? ...
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(58)-DAL层重构
系列目录 前言:这是对本文系统一次重要的革新,很久就想要重构数据访问层了,数据访问层重复代码太多.主要集中增删该查每个模块都有,所以本次是为封装相同接口方法 如果你想了解怎么重构普通的接口DAL层请查 ...
- 帮助对@Repository注解的理解
定义(来自Martin Fowler的<企业应用架构模式>): Mediates between the domain and data mapping layers using a co ...
随机推荐
- 破解iframe微信推文(图片)防盗链
$.ajaxPrefilter(function(options) { if(options.crossDomain && jQuery.support.cors) { var htt ...
- 个人的MySql配置总结
lower_case_table_names参数是用来设置MySQL是否让Schema和数据表大小写敏感,我测试的是在查询界面和MySQL控制台界面无法改变它的值,要在配置文件中改变(先关闭服务),一 ...
- ReactNative实现图集功能
需求描述: 图片缩放.拖动.长按保存等基础图片查看的功能: 展示每张图片文本描述: 实现效果,如图: 实现步骤 使用第三方插件:react-native-image-zoom-viewer 插件Git ...
- Nginx的 HTTP 499 状态码处理
1.前言 今天在处理一个客户问题,遇到Nginx access log中出现大量的499状态码.实际场景是:客户的域名通过cname解析到我们的Nginx反向代理集群上来,客户的Web服务是由一个负载 ...
- class_copyIvarList方法获取实例变量问题引发的思考
在runtime.h中,你可以通过其中的一个方法来获取实例变量,那就是class_copyIvarList方法,具体的实现如下: - (NSArray *)ivarArray:(Class)cls { ...
- php程序员面试经验
面试是你进入公司的第一个关卡,面试过后还会有试用期.可有时候总有那么一些人对待面试完全没人任何防备. 如果你想进入一家优秀的企业,那么对于面试你一定要做好十足的准备.为什么说了:"将军不打没 ...
- Java中静态代码块、构造代码块、构造函数、普通代码块
在Java中,静态代码块.构造代码块.构造函数.普通代码块的执行顺序是一个笔试的考点,通过这篇文章希望大家能彻底了解它们之间的执行顺序. 1.静态代码块 ①.格式 在java类中(方法中不能存在静态代 ...
- C#学习之设计模式:工厂模式
最近研究一下设计模式中工厂模式的应用,在此记录如下: 什么是工厂模式? 工厂模式属于设计模式中的创造型设计模式的一种.它的主要作用是协助我们创建对象,为创建对象提供最佳的方式.减少代码中的耦合程度,方 ...
- C#中DataGridView动态添加行及添加列的方法
http://www.jb51.net/article/72259.htm Datagridview添加列: ? 1 2 3 4 5 DataGridViewTextBoxColumn acCode ...
- Android WebView默认GONE出现的问题记录
前段时间重构一批相似度80%以上的项目[真搞不懂前人们是怎么忍受十几个类似的应用一直CVU的,冗余代码和资源达到40%以上] 其中需要抽出一个公共的带WebView的Activity基类,由于脑残测试 ...