zoukankan      html  css  js  c++  java
  • 我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践

    写在前面

          插一句:本人超爱落网-《平凡的世界》这一期,分享给大家。

      阅读目录:

    第一次听你,清风吹送,田野短笛;第一次看你,半弯新湖,鱼跃翠堤;第一次念你,燕飞巢冷,释怀记忆;第一次梦你,云翔海岛,轮渡迤逦;第一次认你,怨江别续,草桥知己;第一次怕你,命悬一线,遗憾禁忌;第一次悟你,千年菩提,生死一起。

      人生有很多的第一次:小时候第一次牙牙学语、第一次学蹒跚学步。。。长大后第一次上课、第一次逃课、第一次骑自行车、第一次懂事、第一次和喜欢的人说“我爱你”、第一次旅行、第一次敞开心扉去认识这个世界。。。

      第一次的感觉:有甜蜜、有辛酸;有勇敢、有羞涩;有成功、有失败。不管怎样,都要勇敢的迈出第一步,不论成功与失败,至少自己努力过,证明过自己就好,就像哥伦布探索美洲一样,没有勇敢迈出第一步,也许现在“美洲”的概念会推迟不知多少年。

      以下内容,只是一些个人看法和实现,仅供参考学习,也欢迎讨论指教。

    关于DDD

      对DDD(领域驱动设计)最初的了解,始于这一篇博文:http://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html,当时花了四五个小时阅读完,但只是初步对DDD有个了解,有点颠覆自己对编程思想的看法。2004年 Eric Evans 发表 Domain-Driven Design –Tackling Complexity in the Heart of Software (领域驱动设计- 软件核心复杂性应对之道),简称Evans DDD,这本书网上一直没有买到,很遗憾,如果有的朋友有珍藏,可以高价收购。

      什么是DDD(领域驱动设计)?DDD中最核心的是Domain Model(领域模型),和领域模型相对的是事务脚本,领域模型和事务脚本说到底就是面向对象和面向过程的区别。

    • 事务脚本:围绕功能,以功能为中心。将所有逻辑组织在一个单一过程,进行数据库直接调用,每笔交易(业务请求)都有自己的事务脚本,并且是一个类的公开方法。
    • 领域模型:描述领域类,以类之间的协作完成所需功能。所谓领域模型,是一系列相互关联的对象,每个对象代表一定意义的独立体,既可以一起以一种大规模方式协作;也可以小到以单线方式运行。

      好像有个报告统计,大约80%的程序员使用事务脚本编程,三层架构(UI、BLL、DAL)对于我们来说太熟悉了,编程的时候代码一般会集中在DAL层,致使数据访问层充斥着大量的业务逻辑,而且很难复用,每个DAL中的类就像一个单元,只为某一功能实现,也就是上面所说的“单一过程”,因为业务逻辑都实现在数据访问层了,这样业务逻辑层就成了一个空架子,有的人就会觉得BLL-业务逻辑层没有存在的必要,然后设计的时候就把业务逻辑层去掉了,就只剩UI和DAL层了,外加一些HelpClass,然后的然后。。。

      领域驱动设计的概念从提出到现在十年了,现在很少的公司能真正的去应用,而还是采用事务脚本的方式,为什么?其实就是一种思想,或者说方式的转变,就好比你以前习惯用手直接吃饭,现在让你拿筷子吃饭,肯定会不习惯。当然还有一部分原因是领域驱动设计的推行,或者说国内有关这领域的大牛们很少,但我觉得不管怎样,这是个趋势,就像黑夜过后,一定会是清晨一样。

      上面说到三层架构(UI、BLL、DAL),我们再看一下领域驱动设计的分层:

              来自:dax.net

    主要分为四层(表现层、应用层、领域层和基础层):

    • Presentation Layer:表现层,负责显示和接受输入;
    • Application Layer(Service):应用层,很薄的一层,只包含工作流控制逻辑,不包含业务逻辑;
    • Domain Layer(Domain):领域层,包含整个应用的所有业务逻辑;
    • Infrastructure Layer:基础层,提供整个应用的基础服务;

      领域驱动设计主张充血模型,也就是富模型的意思,大多业务逻辑都应该被放在Domain Object里面(包括持久化业务逻辑),而Service层应该是很薄的一层,仅仅封装事务和少量逻辑,不和Dao层打交道。 

      优点:

    1. 更加符合OO的原则。
    2. Service层很薄,只充当Facade的角色,不和Dao打交道。

      缺点:

    1. Dao和Domain Object形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。 
    2. 如何划分Service层逻辑和Domain层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序。 (这个问题在项目实际运作的时候会出现,划分很重要。)
    3. 考虑到Service层的事务封装特性,Service层必须对所有的Domain Object的逻辑提供相应的事务封装方法,其结果就是Service完全重定义一遍所有的Domain Logic,非常烦琐,而且Service的事务化封装其意义就等于把OO的Domain Logic转换为过程的Service TransactionScript。该充血模型辛辛苦苦在Domain层实现的OO在Service层又变成了过程式,对于Web层程序员的角度来看,和贫血模型没有什么区别了。 (和第二点类似,如何做到Application层不包含业务逻辑,协调领域层和基础层很重要。

      领域模型概念参照:http://www.oschina.net/question/12_21641

      领域驱动设计系列:http://www.cnblogs.com/daxnet/archive/2010/11/02/1867392.html

    前期分析

      关于DDD(领域驱动设计)概念有一定了解后,下面开始做一个基于领域驱动设计的项目:MessageManager(短消息系统),至于为什么要拿短消息当小白鼠?是有原因的,当然随便一个业务需求也是可以的,实践是检验理论的唯一标准。

      MessageManager(后面就这样命名)大概类似于博客园-短消息系统,用户模块暂不考虑,只考虑短消息,大致画了一张功能分析图:

      可能当你看到这张图的第一反应是:Are you kidding me???对,你没看错,MessageManager功能就是这么简单,其实领域驱动设计的项目应用应该是一些包含大型业务逻辑的,这种简单的“CURD”操作很难体现出领域驱动设计的作用,但重点不是去实现,而是一个示例框架,可能设计不是很合理,但是一个完整的流程要走下来,当然领域驱动设计包含很多东西,不只是框架设计这一点,很不幸,本篇就只是讨论的这一点。

      MessageManager数据分析图:

      Are you kidding me again???对,你又没看错!!!数据库设计就这么简单,其实不应该说是数据库设计,应该是领域模型设计-数据部分,主要体现在数据库存储,主要是两个表:User(用户表)和Message(消息表),注意我在画图的时候并没有设计字段类型,只是字段名称,类型设计应该在 Infrastructure Layer(基础层)去实现,准确的来说应该是ORM,领域模型只是定义,并不包含实现,有时候我们在做设计的时候,比如ORM使用的是EntityFramework,采用的模式是:Database First,也就是dax.net所说的:

      EntityFramework中的从数据库生成模型”功能应该去掉,但只是相对于领域驱动设计而言,如果项目采用事务脚本,你会发现这个功能是多么的方便,凡事都有相对性。后来EntityFramework推出“Code First”模式,这种模式就符合领域驱动设计思想,MessageManager就是采用这种方式。

      MessageManager的扩展图:

      因为不考虑用户模块,所以用户接入暂不考虑,只扩展一个消息接口,实现方式是:ASP.NET WebAPI,采用WebAPI主要原因是支持REST(无状态),这里需要注意的是此接口虽然是服务,但是属于Presentation Layer(表现层)。关于ASP.NET WebAPI可以参考:http://www.cnblogs.com/xishuai/p/3651370.html

      注:以上前期分析都是按照自己理解去完成,如果严格按照领域驱动设计,应该是建模专家按照严格的流程去做分析的,而不是像我这样随便画几张图。

    框架搭建

      MessageManager主要用到概念或技术点:EntityFramework、ASP.NET MVC、ASP.NET WebAPI、AutoMapper、Nunit、Unity、Unit Of Work、Repository、Specification等等。

      解决方案:

      主要分为四层,可以对比上面的领域驱动设计分层图,当然复杂一点不只分为四层,但是这是最基本的,dax.net在 http://www.cnblogs.com/daxnet/archive/2011/05/10/2042095.html,一文中就增加了很多东西,示例图:

                      来自:dax.net

      XXXX.Repositories项目dax.net在设计的时候放在了Domain中,也就是命名:XXXX.Domain.Repositories,但我觉得仓储实现应该在Infrastructure(基础层)中实现,Domain中只是定义仓储契约,也就是Infrastructure(基础层)中的MessageManager.Repositories,实现仓储的具体实现,并提供持久化操作。

      工作流程描述可以用Unit Of Work一文中画过一张图表现:

    点击查看大图

    代码实现

      MessageManager代码编写主要是四个方面:框架底层、功能实现、单元测试、前端页面。

      框架底层实现可以结合上面那张图和源码去理解,前端页面整理放在MessageManager.WebFiles项目中,页面原始来自博客园-短消息系统,做了一点修改。这边说下单元测试,关于单元测试可以参考:http://www.cnblogs.com/xishuai/p/3728576.html,因为我开发工具使用的是VS 2012,使用的是:NUnit Test Adapter,MessageManager项目中进行单元测试最重要的是Infrastructure(基础层)和Application(应用层),Infrastructure(基础层)主要是对MessageManager.Repositories项目进行单元测试,也就是测试项目:MessageManager.Repositories.Tests,测试主要包含仓储持久化操作,如下:

      功能实现主要是领域模型设计、仓储设计、应用层协调、表现层(MVC、WebAPI)代码编写等,当然还有一些应用程序配置,比如Automapper类型映射、Unity依赖注入配置等。说到领域模型设计,就多说一点,先了解领域模型涉及的概念:实体、值对象、聚合、聚合根。MessageManager项目包含两个实体:User实体和Message(实体),当时设计的时候,我是把User作为实体、Message作为聚合根,也就是下面代码:

    /**
    * author:xishaui
    * address:https://www.github.com/yuezhongxin/MessageManager
    **/
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace MessageManager.Domain.DomainModel
    {
        public class Message : IAggregateRoot
        {
            #region 构造方法
            public Message()
            {
                this.ID = Guid.NewGuid().ToString();
            }
            #endregion
            
            #region 实体成员
            public string FromUserID { get; set; }
            public string FromUserName { get; set; }
            public string ToUserID { get; set; }
            public string ToUserName { get; set; }
            public string Title { get; set; }
            public string Content { get; set; }
            public DateTime SendTime { get; set; }
            public bool IsRead { get; set; }
            public virtual User FromUser { get; set; }
            public virtual User ToUser { get; set; }
            #endregion
    
            #region IEntity成员
            /// <summary>
            /// 获取或设置当前实体对象的全局唯一标识。
            /// </summary>
            public string ID { get; set; }
            #endregion
        }
    }

      Message继承IAggregateRoot,User和Message组成一个消息聚合,聚合根为Message,访问消息聚合内的成员,必须通过聚合根(Message)才能访问,但是在做的过程中,有一个需求就是要通过用户名获取User,如果通过Message访问就很不合理,因为这不包含任何的消息操作,所以后面就把User单独作为一个聚合,聚合根为其本身,这边说明的就是,聚合边界划分不一定一成不变,需要根据具体的业务场景去划分,就比如:做User模块的时候,Message就不能设计成聚合了,而应该是User。

      还有一点就是EntityFramework使用Code First的时候,因为我们“字段”都是设计在Domain层中(并不包含配置),实现却是在Infrastructure层,如何进行数据库字段类型设计?或是表字段关联?实现主要是使用ModelConfigurations,在生成之前添加Model配置,我觉得这是EntityFramework在领域驱动设计开发中优点之一,设计和实现完全区分开,示例代码:

     1 using System.ComponentModel.DataAnnotations;
     2 using System.Data.Entity.ModelConfiguration;
     3 using MessageManager.Domain.DomainModel;
     4 
     5 namespace MessageManager.Repositories.EntityFramework.ModelConfigurations
     6 {
     7     public class MessageConfiguration : EntityTypeConfiguration<Message>
     8     {
     9         /// <summary>
    10         /// Initializes a new instance of <c>MessageConfiguration</c> class.
    11         /// </summary>
    12         public MessageConfiguration()
    13         {
    14             HasKey(c => c.ID);
    15             Property(c => c.ID)
    16                 .IsRequired()
    17                 .HasMaxLength(36)
    18                 .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
    19             Property(c => c.FromUserID)
    20                 .IsRequired()
    21                 .HasMaxLength(36);
    22             Property(c => c.ToUserID)
    23                 .IsRequired()
    24                 .HasMaxLength(36);
    25             Property(c => c.Title)
    26                 .IsRequired()
    27                 .HasMaxLength(50);
    28             Property(c => c.Content)
    29                 .IsRequired()
    30                 .HasMaxLength(2000);
    31             Property(c => c.SendTime)
    32                 .IsRequired();
    33             Property(c => c.IsRead)
    34                 .IsRequired();
    35             ToTable("Messages");
    36 
    37             // Relationships
    38             this.HasRequired(t => t.FromUser)
    39                 .WithMany(t => t.SendMessages)
    40                 .HasForeignKey(t => t.FromUserID)
    41                 .WillCascadeOnDelete(false);
    42             this.HasRequired(t => t.ToUser)
    43                 .WithMany(t => t.ReceiveMessages)
    44                 .HasForeignKey(t => t.ToUserID)
    45                 .WillCascadeOnDelete(false);
    46         }
    47     }
    48 }

      上面代码中的下面部分是添加外键配置,EntityFramework中的模型-添加配置:

    1         protected override void OnModelCreating(DbModelBuilder modelBuilder)
    2         {
    3             modelBuilder
    4                 .Configurations
    5                 .Add(new UserConfiguration())
    6                 .Add(new MessageConfiguration());
    7             base.OnModelCreating(modelBuilder);
    8         }

      下面再说下MessageManager.Application(应用层)的协调配置,先看下面的一张图,注意后面所做的操作都是领域层或是基础层去实现的,并不是应用层实现,应用层只是做协调处理,不要把应用层当做BLL(业务逻辑层)。

                            点击查看大图

    开源-发布

      注:ASP.NET WebAPI 暂只包含:获取发送放消息列表和获取接收方消息列表。

      调用示例:

      WebAPI 客户端调用可以参考 MessageManager.WebAPI.Tests 单元测试项目中的示例调用代码。

      Web 示例页面:

    撰写短消息:

    发件箱:

    查看/回复短消息:

      WebAPI 示例页面:

    后记

      关于时间成本:

    • MessageManager项目:两天(包含晚上)+两个晚上;
    • 本篇博客:一个下午+一个晚上(很晚)+外加更正无数;

      关于DDD实践-MessageManager项目,有几个问题需要记录一下:

    • Domain Model(领域模型):领域模型到底该怎么设计?你会看到,MessageManager项目中的User和Message领域模型是非常贫血的,没有包含任何的业务逻辑,现在网上很多关于DDD示例项目多数也存在这种情况,当然项目本身没有业务,只是简单的“CURD”操作,但是如果是一些大型项目的复杂业务逻辑,该怎么去实现?或者说,领域模型完成什么样的业务逻辑?什么才是真正的业务逻辑?这个问题很重要,后续探讨。
    • Application(应用层):应用层作为协调服务层,当遇到复杂性的业务逻辑时,到底如何实现,而不使其变成BLL(业务逻辑层)?认清本质很重要,后续探讨。
    • 。。。

      因为时间比较紧,MessageManager 项目中很多设计或功能实现不是很合理或完善,比如:异常拦截、日志管理等都没有实现,但走出第一步,就有第二步,第三步。。。

      如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^

  • 相关阅读:
    linux下启动和关闭网卡命令及DHCP上网
    python 编码问题
    paddlepaddle
    Convolutional Neural Network Architectures for Matching Natural Language Sentences
    deep learning RNN
    Learning Structured Representation for Text Classification via Reinforcement Learning 学习笔记
    Python IO密集型任务、计算密集型任务,以及多线程、多进程
    EM 算法最好的解释
    tensorflow 调参过程
    tensorflow 学习纪录(持续更新)
  • 原文地址:https://www.cnblogs.com/xishuai/p/3762826.html
Copyright © 2011-2022 走看看