从我做过的项目来看,似乎欧洲已经有很多的公司开始实施领域驱动设计了,而我本人了解领域驱动设计也有些时间了。但是网上不管是文章还是代码,都显得太过高大上,一谈领域驱动设计,一大堆的概念一股脑的给你上上来,搞的有点晕头转向,而我想在一些中小项目中实施领域驱动也遇到了不小的障碍,大家对很多新的东西总是处于一种恐惧的状态。而且在真正开始实施时,也遇到一些疑问,所以想和大家交流学习,因此开始在此写写对领域驱动设计的理解。后续面陆续会有一些轻量的演进代码。
为何要领域驱动设计?
1简化数据存储
领域驱动设计有很多原因。谈到我为啥要在公司推行领域驱动设计,说起来还是很好玩的。因为原来基于数据驱动的开发方式,也就是传统的多层开发架构,大家定义了一堆DAL来操作数据,在.Net大家一般有两种使用方式,一种是用ORM像Entity Framework, 另一种想使用Dapper这样轻量级的Mapping工具,这些都要把关系型数据转换为对象。结果导致以下几种结果:
- 没有正确的使用ORM, 导致数据加载过多,从而导致系统性能很差。
- 为了解决性能问题,就不加载一些导航属性,但是却把DBEntity返回上层,这样对象的一些属性为空,上层使用这个数据时根本不知道什么时间这个属性是有值的,这个是很丑陋的是不是?
- 如是又开始使用一些轻量级的数据方法,比如使用Dapper,然后自己写SQL语句,这本来是很不错的方式,但是大部分人的SQL能力实在不敢恭维,大部分写出来的SQL语句,甚至比Entity Framework生成的语句还差。
所以,我就想我们做项目,大部分处理的应该是业务,如何让程序员从数据存储,模型转换的大泥潭里解放出来?领域驱动设计就进入了我的视线。当然光从数据这个角度还不足以选择领域驱动设计,用一个NoSQL数据库是不是就解决了?但是NoSQL也有一些问题,比如MongoDB如何更优雅的保证事务以及数据的一致性等。
2更多了解上下文
我们很多软件的问题,大家都知道是需求的问题,也就是客户的需求我们很难理解准确。程序员也更加关注“HOW”——如何实现需求,而忽略了“WHAT”——什么是需求。最终做了几个礼拜甚至更长时间,客户却说:“What?! I told you.”但是客户告诉我们的,跟我们理解的是不一样的。比如客户说:“Great job, I love you!”这个Love肯定不是男女之间的Love。我们拿到的是一个客户的需求,但他的上下文是什么?比如说:“这个球打的好”,如果是在打篮球,肯定说的是篮球;如果是在打乒乓球肯定说的是乒乓球。而领域驱动设计里我们可以让业务人员更多的参与系统,更早的参与系统。
3统一语言(Ubiquitous Language)
业务人员和我们使用一样的语言。我们的程序,比如让业务尽量集中在领域里,比如在传统的数据驱动里,如果说Jack爱Rose,我们一般会这么写: C# UserService.Love (Jack, Rose)
但是我们业务人员很奇怪谁Love谁?为什么要UserService?如果我们写成这样:Jack.Love (Rose);
还有如果我们用:Company.hire (employee) 来代替companyservice.hire (company, employee), 这样我们就更容易让业务人员参与进来,而且代码可以更易于表示真实的业务场景。
领域模型
领域驱动设计里有很多东西,我们可以应用在各种各样的开发模式里,所以接下来说的一些东西,我们可以部分使用。
说到领域驱动的领域,大家肯定就要开始说Bounded Context,聚合,聚合根,容易把大家搞糊涂。我觉得应该先抛开这些概念,后面再来说如何设计聚合。先从简单的开始。
模型
过去,我们在多层设计里定义了很多Model。比如数据库的Model (DB Entity),然后为了不依赖数据库,我们又设计了业务的Domain Model,同时我们又设计了View Model,这样一般也没什么问题,职责也很清晰。但是有几个问题:
- 我们要做很多的模型转换,转入转出。当然我们可以用AutoMapper来处理,但是AutoMapper的性能实在难以恭维,大家可以在网上搜索AutoMapper performance看看。
- 领域模型成了一个单纯的DTO了。
领域模型
首先我们要看领域,就是我们尽量把业务聚合到一个领域里。比如我们要做一个功能,可以看到用户每一次的登录日志,那么这个登录日志其实就是属于用户这个领域里。
其次我们看模型,原来我们的模型都是只有属性,也就是贫血模型,贫血的意思就是没有行为,像木乃伊一样。但是实际上领域是我们要完成业务的最主要的地方,我们希望领域能够自治,也就是领域自己管理自己。
示例
比如有一个Employee,他的状态有Active,Pending,DeActive,业务上是Pending只能改为Active。
如果是贫血的Employee模型,我们往往代码如下:
但是上面的代码的问题就是领域没有自治。本来修改我的状态是我的事,你不能修改,外面随意修改我的状态是很危险的,比如Pending状态只能改为Active状态。所以如果不是贫血的模型,我们代码就会这样,让领域自己来管理。
因此可以看出,我们把业务代码尽量写在领域里让领域自治。
后记
其实领域驱动设计最难的就是设计领域 (Domain),也就是后面会说到的AggregateRoot 聚合。但是我想我们先让领域不再贫血,这样在传统的多层设计,数据驱动等架构里都可以使用这种模式。
事件驱动
今天主要讲一下如何用事件来解耦,我们有个项目的一个功能我觉得用事件的方式比较好,正好写篇博客,就不用专门给他们讲了。
解耦
说到解耦,我们很熟悉分层设计,比如上层依赖于抽象,不依赖于具体的实现。比如一个类使用另一个类,我们使用接口而不直接使用实现类。
为何用事件
1.SRP (单一职责)
比如我们一个会议室预定系统,我们的一个设备坏了。我们需要通知预定这个会议室的所有人。于是我们需要发邮件。
伪代码如下:
但是,问题来了。如果后来我们要说,如果设备坏了,我们要更改可用库存的数量,这时候我们是不是要在这里修改代码而引入InventoryService? 后来如果经理说设备坏了你们竟然不告诉我,你们要闹哪样?这个时候我们是不是要修改代码引入ISMSService.Info(Manager)?即使我们不考虑OCP原则,不考虑单一职责,我们程序员也会哭,我就DeActive一个设备,你要我做这么多事,我哪里清楚所有的功能?我就骂过程序员,你做这个功能呢为什么没考虑全!!!漏掉了这么重要的功能。
而问题是,程序员从来没考虑全过,因此我就想办法如何解决程序员不仔细的这个问题。
2. 事件驱动
因为我熟悉iOS的开发,我就想到了iOS的Notification Center. 那我DeActive一个设备,我就只DeActive这个设备,很SRP是不是?但是别的地方如何拿到通知?于是事件就自然的浮出水面了。如果设备被DeActive了,程序就只需要喊一声,老子把设备DeActive了,你们要闹哪样你们自己看着办,代码如下:
这样,通知会议室预定者的模块去通知预定者,给老板发短信的模块就通知老板就OK了。
以上只说为什么要使用事件驱动。但是只有概念是不够的,我们要代码呀!记得脸书的老总说过: “Talk is cheap,Show me the code!”所以接下来就show一下code。
实现思路
1.发出事件
事件顾名思义就是一件事情发生了,比如我要上头条,这不是一个事件,这是一个Command, Head Command,而我上头条了这就是一个事件Headed Event,事件就是一件事情已经发生了。好,先来一个伪代码:
所以我们只需在代码里Raise Event就可以了。
2.如何订阅事件
其实很简单,因为我们要实现的是同步的事件,我们只需要找到所有处理这个事件的实现类,然后调用所有就可以了。
如果国际章的妈妈关注这个Event, 我们就实现一个GuoJiZhangMotherEventHandler
如果我等屁民也关心这个事件的话,我们只需要再实现一个 PiMingEventHandler
看,我们可以任意增加关注事件的代码,不用修改原来的代码吧,说好的OCP没骗你吧? 那么问题来了,发出事件的人和接受事件的人怎么联系上的?在现实世界中,我们都是订阅报纸来看头条知道的,但是代码里我们就需要一个协调者了。如是我们就需要一个EventBus, 直接上代码吧
EventBus找出所有Handle这个事件的实现类,调用对应的Handle方法,我们可以通过Castle或者任何注入框架轻易的实现。
好了,哥只负责帮汪老师上头条,上完我发出了事件通知,谁关注谁自己处理去,我的代码也不用改。
我代码实现完了,如果各位还不知道如何实现一个同步的事件驱动架构,那拜托你们找个漂亮的妹子来问我。事件驱动架构我就只能帮你到这里了。