领域驱动设计也就是3DDomain-Driven Design)已经有了10年的历史我相信很多人或多或少都听说过这个名词但是有多少人真正懂得如何去运用它或者把它运用好呢于是有人说DDD和TDD这些玩意是一些形而上的东西只是一茶余饭后的谈资又或是放到简历上提升逼格而已。前面这句话我写完之后犹豫了犹豫要不要把它删掉因为它让我看起来像个喷子我确实感到不解为什么别人10年前创造总结出来的东西我们在10年之后对它的理解还处于这么低的一个层次。开篇就说远了我也是最近才开始认真学习领域驱动设计并且得到了园子里面netfocus,刘标才和田园里的蟋蟀的帮助在此再次表示感谢。希望能和大家一起把DDD普及下去。我们之前有一个关于领域驱动设计的讨论另外dax.net也有一个关于领域驱动设计的系列写得不错有兴趣的同学可以看看。本文会以一个初学者的角度来讲解DDD让我们一切从零开始我相信你跟我一样也会爱上它的。本篇主要讨论一下为什么我们要用DDD它能够为我们带来什么领域驱动系列初探领域驱动设计1为复杂业务而生初探领域驱动设计2EF 和 Repository初探领域驱动设计3写好单元测试......目录一点废话我们需要好的设计么从设计阶段出发 - 站在业务的角度思考问题建模区分实体、值对象和领域服务厘清业务关系 - 聚合与聚合根独立领域业务层 - 高内聚低耦合可测试干净漂亮的代码小结一点废话 我们需要好的设计么当我们学习一些设计模式或者框架的时候总有人会站出来和你说“这些都没有用只要能实现功能就行了。” 在这里并非针对某个人实际上我认为他们说的是对的在资源有限的情况下我们为了完成项目的交付这是我们最好的选择。但是别忘了欠下的债总是要还的以实现功能为导向的项目务必会造成维护性的大大降低如果只是一个临时随便用用的东西倒是可以一试但如果是要长期进行更新的产品那后期就会拖该产品的后腿。我们团队现在维护着一个有着20多年历史的产品该产品是一个酒店、餐饮行业的POS系统在美国和亚太地区都占有着比较大的市场份额。该产品从CCVB6一路更新直到现在的C#但是很可惜不是整体替换而是局部的所以现在项目里面这4种代码全都有。可能你会觉得这玩的是混搭是潮流但事实是一旦产品上线之后会有很多的新功能老bug等在那里再加上“重市场轻技术”的高层在那里制订战略你压根就没有时间或者没有多少时间去重构。日积月累等着你的就是每一次改代码都如履薄冰一不小心就因为改一个bug而整出好几个新bug出来前不久我们为了新版本的发布就停下所有开发的任务大家集体花了1个月的时间去做回归测试了。因为前期发布新版本之后bug太多所以这次老大们都不敢轻易发布了。:)这是我们血的教训如果你前期只顾开发功能最后就会让你很难再开发新功能。所以真诚的希望大家不要再片面的说“只要实现功能就可以了”软件开发的领域这么大我们没有必要把自己局限在某一个框框里面。对于大型系统来说我们要学习的地方还有很多组织良好、可阅读性高的代码可以让其它开发人员很容易的开始去修改代码。低耦合高内聚 - 适合运用设计模式以及原则来设计一些好的框架可以降低修改代码引发新bug的风险。良好的单元测试以及集成测试可以及时的帮助我们检测新增或修改的代码是否会破坏原有的逻辑。自动化测试绝对是省时省力的好帮手也是项目质量的保证。持续集成可以帮助我们更快速安全的进行迭代。上面说了这么多也没有提到DDD那么为什么它能够在构建复杂系统的时候有优势呢我们可以从以下几个点去思考从设计阶段出发站在业务的角度思考问题厘清业务主次独立领域业务层打通开发和测试阶段干净的代码从设计阶段开始站在业务的角度思考问题除了DDD现在还流行另外一个词汇TDD。但是不知道大家有没有注意到DDDDomain-Driven Design)中的D代表着设计而TDD(Test-Driven Development)中的D代表着开发你有没有曾几何时把领域驱动设计说成领域驱动开发呢当然我们确实是可以根据领域驱动来开发但是DDD被设计出来的完美初衷却是设计。TDD强调的已经是开发了要求开发人员先写单元测试然后再通过不断的迭代重构让单元测试通过以此来实现功能。这样做的好处是强迫让开发人员清楚正确的理解需求要知道这年头没有正确理解需求就开始写代码的程序员大有人在并且我不认为需求就是业务需求已经是将本来的业务理解之后转化为了通过计算机可以实现的一些功能定义通常是业务分析师或者项目经理会去完成这个工作。而DDD中的D领域更像是本来的业务所以在领域驱动设计的时候开发人员或者架构师直接与领域专家或者说客户进行沟通来建模这些业务模型也是以后开发人员进行设计和实现的依据。领域模型被当作开发人员之间开发人员与领域专家之间沟通的桥梁这样可以闭免开发人员用错误的方式去实现功能。实际上很多优秀的开发人员都会很自然的将现实世界中的问题进行抽象然后用计算机的语言表示出来我们称之为面向对象。但是由于缺少亲临其境的体验往往会离真实的业务模型有一些距离。我们举一个例子来说明一下这个问题假如我们要开发一个电子商务的网站这个需求已经非常清楚了现在那么多的电子商务网站直接照抄一个就可以了。现在我们来做一个下单的功能来看看怎么去实现 。作为一个高级程序员我们得用面向对象的方式去开发先建类。于是我们有了用户订单订单项的类用户创建订单然后往订单里面添加商品添加订单项的时候为了方便我们只需要传入产品ID和数量就可以了于是Order类有一个AddItem的方法。作为一个高级程序员一看这图感觉很完美有木有 好下面开始实现AddItem方法。Order里面是一个OrderItem的集合而这个AddItem的方法接收的是productId我去哪里搞个Product对象给你我不可能在这个实体里面直接去查数据库吧本来是冲着这个技术点想咨询一下大家后来在小组里面讨论了一下我恍然大悟上面的实体就是我从代码的层面去思考想出来的下单嘛当然是用户订单和订单项喽。可是只要去网上买过东西都知道用户是不会直接往订单里面加东西的而是先把商品加入购物车然后再通过“结算”一次性就根据购物车生成了一张订单压根没有往订单里面添加订单项的行为。这才是真正的用户行为领域逻辑所以后来我们的实体变成这样了所以业务是这样的未注册用户也可以将商品添加到购物车中但是不能下订单。并且购物车中的商品不能保存起来用户离开这个网站一般是关掉浏览器购物车中的商品就会消失。注册用户购物车中的商品可以长期永久保存通过购物车的“结算功能”将购物车中选中的商品转化为订单。所以购物车应该在用户注册的时候就应该创建好对应我们上面的User实体中的CreatShoppingCart()方法。下面我们先来简单实现一下注册的代码。//User领域实体代码View Code//领域层 UserService.cs代码View Code//应用层 UserService.cs代码View Code上面是我们一次建模的过程是一个将业务转变成代码将现实世界抽象成软件世界的过程。我们需要画出模型不断的与业务人员领域专家去沟通然后不断的重构去完善我们的模型以至于这个模型能最准确的反映真实的业务。这是在最开始的设计阶段是需求沟通阶段就需要做的工作并且会一直贯穿我们后面的开发甚至维护阶段没有人可以一开始就把领域模型建的100%准确需求是复杂的并且需求还是随时变化的所以模型也会一直发生改变。它将作为开发人员与业务人员、测试人员以及开发人员自己之间沟通的桥梁。而DDD与其它方法论的区别之处就在于它还提供了一整套的体系来保证后续对领域模型的重构不会让系统变得四分五裂比如架构分层仓储依懒注入等等我们后面再慢慢探讨。在DDD中领域模型分为三种实体值对象领域服务区分实体、值对象和领域服务我们不打算去解释以上的概念我相信只要你搜索一下就可以得到很全面准确的答案。但是重要的是我们一定要理解3者之间的区别什么时候是实体什么时候是值对象又是什么时候我们该用领域服务呢我想这是刚接触DDD的人都难免会有些纠结的地方吧在这里就强调一下。实体相对于值对象而言拥有“标识”的概念标识可以让我们持续性的跟踪实体。标识和数据库里面的“主键”是不一样的概念主键是技术上的概念但是标识是业务上的概念。在我们上面的例子中用户ID是标识我们用它来持续性的跟踪我们的用户。订单ID是标识我们用它来持续性的跟踪订单同时我们的用户和订单都是有着不同的状态。但是对于用户的地址来说我们用什么来做标识呢在电子商务网站这样的业务里面我们不需要去持续的跟踪这个地址信息它在我们的系统里面也不会有着像订单从“创建”、“已付款”、“已发货”、“已收货”等这样的状态所以地址信息的我们系统中就是一个值对象。但是我们如果换了一个系统比如说死慢的长城宽带他们把地址作为跟踪对象。同一个地址谁都可以去注册但是同一个时间只允许一个人去注册那么这个地址对于长城宽带来说就去要去持续性的跟踪有“开户”“销户”的状态。那么地址信息对于长城宽带来说就是一个实体。解决完实体和值对象领域服务就好说了一些重要的领域操作既不属于实体也不属于值对象那就可以把它放到服务中了。比如说我们上面的领域服务UserService里面的注册操作注册这个操作可以说就是将这个用户保存到我们的系统中。在注册之间这个用户是不存在的我们又怎么能把注册这个操作放到User实体中去呢所以把它放到领域服务中成了我们最好的选择。即使是这样哪些操作应该放到领域服务中对于很多初学者来说还是一件比较难选择的问题。也许只有慢慢的对业务越来越了解对DDD应用的越来越熟我们就会少一点纠结。厘清业务主次-聚合与聚合根在上面的模型中我们有很多关系的存在用户-购物车1对1用户-订单-订单项-产品1对多1对1购物车-购物车项-产品等。在DDD中我们把这样多个模型用关联串起来组成一个聚合(aggregation)。在我们的模型中购物车-购物车项是一个聚合订单-订单项是一个聚合。我们通常需要保护这些聚合的一致性比如说我们把一个订单删掉了那么这个订单的订单项也需要一起删除否则他们存在也没有任何的意义。以前我们还会用到触发器但是大家都知道这个东西维护起来比较麻烦写起来也不方便等所以后来大家都是在代码中来控制。但是一直没有一个好的约束说我们如何去更好的控制这些一致性代码一直都很散乱直到DDD我们有了聚合和聚合根的概念“我们通过为每一个聚合选择一个根并通过根来控制所有对边界内的对象的访问。外部对象只能持有根的引用由于根控制了访问因此我们无法绕过它去修改内部元素。我们后面还会说到只能为根来建立Repository这也是为了确保我们这里面讲的数据的一致性。在我们上面的聚合中只能通过购物车实体来操作购物车项而不能你自己写一个保存的方法直接就把购物车项给保存到数据库中去了。这就是聚合和聚合根起到的作用。我们来看一下我们购物车实体的代码View Code大家可以看到我们购物车实体的逻辑很清晰因为我们很明确购物车拥有哪些操作。当然还有另一种做法即把这些操作都放到用户实体中去因为最终其实是用户做的这些操作。那我们的聚合就变成了用户-购物车-购物车项这样也没有什么不可以反而更符合真实的场景。但是会导致我们的聚合过庞大也就是说我必须要先有用户实体才能进行操作用户用户可能会绑上很多的东西购物车、订单、地址等等。在现在都是ajax来操作的大型网站中我们需要在服务端把这个用户请求加载出来再执行添加购物车的操作呢还是可以直接加载购物车实体来操作呢这就是一个粒度的问题不同的问题和场景大家可以区别来对待。总之聚合是可以根据业务或者一些特定需求来做出调整的。比如说购物车-购物车项-产品这也是一个聚合但是由于产品的特殊性我们可以把产品也作为一个聚合根来单独进行访问。我们来看一下应用层ShoppingCartService的代码View Code此应用层代码一出大家就会发现这代码太简洁了有木有因为所有的逻辑、业务都被放到领域实体那里面去处理了。即使我们业务逻辑改变了或者我们需要重构了它们都在领域实体那里面改那里就好了。接下来的问题是如何确保安全正确的一次又一次的对领域实体进行重构呢毕竟它也是各种关联各种依懒呀您请接着往下看我们的单元测试环节。独立领域业务层 - 高内聚低耦合可测试讲到这里请允许我从网上盗一张图当然这张图早就已经是被引用过无数次了它就是DDD中使用的分层结构。关于这个分层每一层是干什么的具体怎么玩大家可以看一下dax的这一篇文章讲解的很清楚。总之我们的领域模型以及相关的类比如工厂等会被独立成为一层来与应用层和基础设计层交互。领域层是独立的首先它是应用层的下层所以肯定不会有对应用层的依懒但是领域有一些模型或者服务少不了是要与数据库打交道的比如说我们在注册用户的时候需要去验证当前的邮箱是不是已经被占用了。而这一类操作都是属于基础设施层做的事情包含像一些数据库操作日志缓存等等。那么我们如何避免领域层对基础设施层的依懒呢感谢面向对象设计 - 面向接口编程只不过这里面的场景特别有代表性它是一个非常常见的问题于是它成为了一个模式仓储Repository)。View Code一般情况下我们会把仓储的接口放到领域层或者也可以再建一个Core层来作个项目最下面的那一层提供一些最公共的组件部分。关于仓储的代码大家在上面领域服务UserService中的注册代码中就已经见到过了。可能需要注意的是Repository用来将数据库与其它的业务和技术分离所以我们在领域层中使用它还在应用层中使用它。Repository让我们专注于模型不用去考虑持久化的问题。更为重要的一点是因为它是接口所以我们可以很方便的替代它或者模拟一个实现来对我们的领域模型进行单元测试。下面是我们实现的MockRepository的代码View Code下面我们给我们User领域实体的注册方法加一个检查Email是否存在的逻辑。View Code在我们真实的Repository出来之前不管我们是打算是EF还是NHibernate我们现在只要对这个Mock的Repository来编程或者进行单元测试就可以了。//UserService领域服务在单元测试View Code我们用的XUnit.net作单元测试框架同时用了Fluent Assertions。结果很漂亮有木有有了单元测试来为我们的领域模型保驾护航我们就可以安全的进行重构了。干净漂亮的代码经常有人说代码是一件艺术码农都是艺术家。我很喜欢这句话如果你也认同那就请像对待艺术品一样对待我们的代码精心的打磨它。并且你不一定要非常的有经验才可以干这件事情如果你刚入行那至少保证一代码可读性好好的命名代码逻辑清晰等再往上一点你要能够更好的组织代码类函数等到你也成为专家了那就开始考虑一些重用性可扩展性可维护性可测试性的这些比较范的东西了而最后就上升到架构层面考虑系统各个组件之间通讯分层等等。最后你就成为码神了。DDD里面引入的一些思路包括分层、依懒注入、仓储等可以给我们一些指导大家从上面的代码也可以看出这些代码组织的很好逻辑也不会散乱的到处都是。当然这个项目代码量有限说服力是有限的后面我们还会尝试去加入应用层的代码。代码已经放到CodePlex上去了http://repositoryandef.codeplex.com欢迎大家Follow。注意代码还没有写完只是一个初级版本我们后面会慢慢完善。这个项目会使用EF来作业ORM框架Autofac作依懒注入容器用Xunit作单元测试框架的同时引入了Fluent Assertions。小结本文主要介绍了DDD的一些基础概念领域模型领域实体、领域服务以及值对象建模一定要从真实的领域业务出发多与领域专家进行沟通来完善模型。聚合与聚合根它的主要作用是用来确保各种关系下的实体的数据一致性但是确认聚合根这个过程实际上也是对业务的梳理过程。架构分层 每一层都职责清楚依懒于接口来降低耦合。封装和测试 所有的业务都放到领域层同时对领域层进行单元测试来确保最核心的逻辑不会遭到破坏。个人感觉没有必要太强调Repository的概念从领域实体的生命周期创建-持久化到数据库-销毁-从数据库重建你会发现其实这个过程很普遍并不是只有DDD才有的。所以我认为Repository主要是将数据访问功能给隔离开避免领域实体对基础设施层的依懒。那它和三层有什么区别 BLL 引用DAL不也是依懒于接口么给我的感觉是DDD的领域实体持久化这一块就是三层里面的思路。这可能是在学习DDD初期的想法因为真实的大型项目中是不会直接把领域实体给持久化的那个叫DTO于是Repository里面放的就不是我们的领域实体了而是将领域实体转换成对应的DTO。是否一定要使用DTO呢领域实体和DTO互相转换最后到了表现层DTO还要和ViewModel转换会不会带来复杂性和性能上的损失Repository和EF还有Unit Of Work怎么来协调抱怨写单元测试么怎么样让写单元测试不变成只是走过场而已 这些问题留给我们后面再解决吧。作者Jesse 出处 http://jesse2013.cnblogs.com/本文版权归作者和博客园共有欢迎转载但未经作者同意必须保留此段声明且在文章页面明显位置给出原文连接否则保留追究法律责任的权利。如果觉得还有帮助的话可以点一下右下角的【推荐】希望能够持续的为大家带来好的技术文章想跟我一起进步么那就【关注】我吧。