写在前面
关于“Repository 仓储,你的归宿究竟在哪?”这个系列,本来是想写个上下篇,但是现在觉得,很有多东西需要明确,我也不知道接下来会写多少篇,所以上一篇的标题就改成了《Repository 仓储,你的归宿究竟在哪?(一)-仓储的概念》,在这篇博文中,主要讲了仓储的概念,并没有探讨有关仓储归宿的任何东西,但你发现,后面评论中的探讨会比博文内容更有价值,这也是我所坚持写博文的目的之一,也就是分享的价值。
上一篇博文评论中,大部分内容是我和 czcz1024 探讨“Specification-规约”,我也不知道怎么会扯到这个话题上了,就像 netfocus 兄最后所说:有点偏离主题了。确实如此,本来我觉得这一篇博文也就这样了,但是最后刘标才给我回复:
我想问下,领域模型里面到底要不要去引用仓储接口呢,好像ddd那个关系图里面是可以调用的,如果实体不引用的话,一些复杂的逻辑在实体里面根本无法实现,或者只能被分割成2部分,一部分在实体,一部分在appcation中,appcation中的一般是查询方法;比如有一个登记的业务方法(其实就是insert了),在登记前需要判断当前年份的金额是否大于登记实体的金额,大于那么可以登记,否则异常,类似这样的业务,判断查询那个到底是在实体里面判断还是在appcation里面判断呢,如果是实体的话,那么仓储接口怎么注入到实体,实体是new出来的????
如果不嫌多,我再贴出一段:
这个问题我已经纠结很久了,还没有看到具体是怎么样实现的,stackoverflow里面也有很多老外在讨论,但也没有最终的答案,大部分人认为没有必要在实体或者聚合中使用仓储(或者注入仓储);我个人认为是一定要的,先不说这样做对不对,我们可以根据项目的实践去看问题,很多业务都需要判断一下是否唯一,面积或者金额之类的是否足够,这些都是需要查询才可以得到值的,如果是放在application去实现,就等于把这些业务转移到了上层,而且随着业务的复杂度增加,这样的实现就会很多,如果放在领域层实现,看起来是很不错的选择,但是实体或者聚合里面怎么去注入仓储接口呢,实体和聚合都是被new出来的,看起来是没有办法注入了,只能在实体里面调用XXX注入类.GetInstance<仓储接口>();这样的话领域层就要依赖某个注入框架了,所以这个仓储问题,不管放哪都不那么完美。
我当时看到这段回复的时候,我觉得我找到知音了,为什么?因为只有实践过,你才会感同身受,大道理说一大堆,不去实践应用,这种问题你不会发现的,更不会去思考怎么解决?
我为什么认死理,非要探讨仓储的归宿?
其实关于这个问题,netfocus 兄看到,应该会非常无语(哈哈),因为早在《设计窘境:来自 Repository 的一丝线索,Domain Model 再重新设计》这篇博文中,我和他就曾探讨过,当然还有之后其他的一些交流,他的意思是:为什么非要纠结在仓储这一块?如果职责划分的比较明确及正确,那该怎么使用就怎么使用,不管是应用层或是领域中,只要符合,那它就是正确的。探讨的问题是“领域服务中能不能使用仓储?”,这个也是我之前一直纠结的地方,其实 netfocus 兄的意思我都懂得,这也是仓储设计的大前提之一,那就是职责或边界划分清楚。
后来 Luminji 兄发表了一篇博文《面向对象架构模式之:领域模型(Domain Model)》,看完博文内容,再看评论,你会觉得这完全没有相干性(还是有一点的),评论中主要探讨的还是仓储的归宿(调用问题),但是到评论结束,还是没有一个准确的结论,为什么?因为大家都没有去实践应用,也就是没有针对一个具体的业务场景进行探讨,比如针对某一个业务用例,把仓储的归宿放在领域服务中,那这个仓储具体该怎么设计实现?怎么调用?怎么配合领域服务完成一个具体的业务用例?应用层的代码又该是怎样的?IOC 容器怎么去注入?。。。虽然是一个“很小”的问题,实践应用过后,你会发现,其实这是一个很大的问题,当然前提是,你要去实践,去应用。
最近,Jesse Liu 兄在小组中发布了一个话题《讨论一下领域驱动设计》,我觉得这种探讨非常棒,因为大家都是针对同一个具体的业务用例,而不是各个不同的业务用例,而且这种探讨会让你学到,别人在这种业务用例下是怎么进行领域驱动设计的?不自觉会纠正你的一些错误观点,当然前提是,你不是偏执的人。
以上我所叙述的一些东西,我个人觉得都是停留在理论阶段,就像 Jesse 兄的那个话题,如果针对购物车这个业务用例,接下来的设计会是怎样?因为之前的探讨内容都是职责和边界,其实并没有去实践与应用,如果实践了,你会发现这其中的一些其他问题,“仓储的归宿”,只不过是这些问题的其中之一。
这样的应用层代码,你能接受吗?
言归正题,关于“仓储,你的归宿究竟在哪?”这个问题,这篇博文我想晒一下,我现在应用层的代码,业务场景还是短消息系统,业务用例是发送短消息,代码如下:
public OperationResponse SendMessage(string title, string content, string senderLoginName, string receiverDisplayName)
{
using (IRepositoryContext repositoryContext = new EntityFrameworkRepositoryContext())
{
IContactRepository contactRepository = new ContactRepository();
IMessageRepository messageRepository = new MessageRepository(repositoryContext);
ISendMessageService sendSiteMessageService = new SendSiteMessageService();
Contact sender = contactRepository.GetContactByLoginName(senderLoginName);
if (sender == null)
{
return OperationResponse.Error("抱歉!发送失败!错误:发件人不存在");
}
Contact receiver = contactRepository.GetContactByDisplayName(receiverDisplayName);
if (receiver == null)
{
return OperationResponse.Error("抱歉!发送失败!错误:收件人不存在");
}
try
{
Message message = new Message(title, content, sender, receiver);
if (messageRepository.GetMessageCountByIP(System.Web.HttpContext.Current.Request.UserHostAddress) > 100)
{
return OperationResponse.Error("一天内只能发送100条短消息");
}
if (messageRepository.GetOutboxCountBySender(sender) > 20)
{
return OperationResponse.Error("1小时内只能向20个不同的用户发送短消息");
}
if (sendSiteMessageService.SendMessage(message))
{
messageRepository.Add(message);
return OperationResponse.Success("发送成功");
}
else
{
return OperationResponse.Error("发送失败");
}
}
catch (Exception ex)
{
if (ex.GetType().Equals(typeof(ArgumentException)))
{
return OperationResponse.Error(ex.Message);
}
CNBlogs.Infrastructure.Logging.Logger.Default.Error("Application_Error: SendMessage", ex);
throw ex;
}
}
}
Are you kidding me?没错,你没看错,这就是现在短消息项目中应用层中的一段代码,对于 DDD 的狂热爱好者来说,我觉得他们看到这段代码,肯定会抓狂的。。。
虽然短短几行的代码,但这其中所暴露出来的问题,实在太多了(比如仓储上下文设计、自定义异常处理、仓储的定义等等),其实我觉得你最不能接受的应该是,中间那两个发送消息之前的业务验证:
- 一天内只能发送100条短消息。
- 1小时内只能向20个不同的用户发送短消息。
这个是属于业务规则,怎么会放在应用层?难道我脑袋锈掉了?当然没有,这个我原来是想放在 SendSiteMessageService 领域服务中的,但是我原来的设计是领域服务中不进行仓储的调用(为了保持领域的纯洁),包含业务用例描述,所以,针对这两个业务验证,是没办法放在领域服务中的,因为这种涉及到到领域对象的读取,而所有的领域对象读取接口都设计在仓储中,领域服务想进行业务验证,又不想进行领域对象读取,你觉得可能吗?
其实这种问题,有两种解决方案:
- SendSiteMessageService 领域服务中实现仓储的调用。
- 领域对象的读取放在应用层中,获取之后交由领域服务进行验证。
我个人觉得,第二种实现方式只能针对一定的业务场景下,如果在业务验证过程中,又涉及到领域对象的读取,这个实现方式就有点不合理了,而且获取领域对象的操作,其实也是业务的一种体现。
代码设计是一方面,代码重构又是另一方面,后一个过程要比前一个过程困难百倍。
写在最后
这篇博文,我不希望写的太长,核心内容就是那段应用层中的代码,我知道兄台你已经发现问题了,那就请兄台大声的说出来吧。
领域驱动设计中,我再列一下有关仓储的一些探讨博文: