驾一叶之扁舟 举匏樽以相属
寄蜉蝣于天地,渺沧海之一粟。哀吾生之须臾,羡长江之无穷。
挟飞仙以遨游,抱明月而长终。知不可乎骤得,托遗响于悲风。

从壹开始微服务 [ DDD ] 之二 ║ DDD入门 & 项目结构粗搭建

前言

 

哈喽大家好,今天是周二,我们的DDD系列文章今天正式开始讲解,我这两天一直在学习,也一直在思考如何才能把这一个系列给合理的传递给大家,并且达到学习的目的,还没有特别好的路线,只是一个大概的模糊的安排,毕竟我没有做过讲师,但是我感觉还是需要对自己负责,至少要对得起这个熬夜写的博客吧 😀,我简单设计了下整体流程,可能以后还会变动,不过大致方向是不会变的:

我打算通过一个最简单一个例子来讲,而且这个例子也是《实现领域驱动设计》这一本书的第一个小例子,至于为什么要引用这个小栗子,我想的是,希望把整本书的大致精髓柔和到我这个 Project 中,最好给大家一个感觉,以后大家再看的的时候,不会感觉在天上飘飘然不知其所往,相反,是会感觉似曾相识,会感觉看过,不会那么生硬无趣,这个就是我希望达到的两个目的的其中之一:在了解并认识DDD领域驱动设计的情况下,也简单的了解了这边书的相关内容。

今天要说的是我们平时百分之百会遇到的,就是新建一个实体类,并对其进行CURD操作,看到这里,你也许会胸有成竹的说,已经玩儿的很溜了,当然这是肯定的,今天不是说哪些新知识,而且说一下其实DDD驱动设计,已经在我们平时的开发中体现出来,当然又没有完全贯彻下来(至少我是这么一个),也说一下在我们的开发当中为什么需要使用DDD领域驱动设计。

这里说明下今天搭建的是一个小雏形我会以后慢慢细化,像上一个系列那样,逐渐增加功能,差不多最后的框架是这样的:

 

这里要说明下:
1、平时我们的小项目中,用户故事和用例流 小于20 的,就不需要使用DDD了,至少是一个中型电商系统以上这种才需要,如果你就一个控制台,或者日志管理系统,那就不需要了。

2、学习新东西不要拘泥于技术,应该重点理解思想,思想不通,就不想学,就会打心里排斥。

更新:

下边的评论很精彩,希望大家看看,更希望大家来讨论讨论,好多人说 DDD 领域驱动设计就像多层架构,感觉又像是一个 MVC 架构,我这里要说的是:

多层和MVC 都是一个成型的,很好的框架,注意这里是框架,像一个梯子,简单+方便+高效+多年验证可行,但是这仅仅是一个框架,我们拿到一个项目后,就迅速搭建架构了。

但是DDD领域驱动设计,是一个设计思想,更像是一个全自动高效电梯,里边有算法,有丰富的子领域,我们把专注点放到领域如何划分,如何领域建模,把一些具体的操作封装在了一个虚实体中,然后同时在写实体的时候继承这个虚实体,提供了值对象,给对象增加了状态等等。当然以后还会有驱动设计,领域+驱动,才是DDD,今天就是一个简单的领域建模,大家多思考思考。

DDD最大的好处是:接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。DDD让你首先考虑的是业务语言,而不是数据。重点不同导致编程世界观不同。DDD是解决复杂中大型软件的一套行之有效方式,在国外已经成为主流。DDD认为很多原因造成软件的复杂性,我们不可能避免这些复杂性,能做的是对复杂的问题进行控制。而一个好的领域模型是控制复杂问题的关键。领域模型的价值在于提供一种通用的语言,使得领域专家和软件技术人员联系在一起,沟通无歧义。

 

 

零、今天要实现橙色的部分

 

 

 

一、为什么要使用DDD

这个已经是老生常谈了,相信大家也稍微或多或少的了解一些,在领域驱动设计中,我们的中心不在代码是开发技术上,而且在业务上,在这个设计中,会涉及到开发人员和领域专家(可能是项目管理者,或者部门经理,甚至是公司总经理,这些都可以算上领域专家),虽然在使用DDD的时候,需要我们在前期投入较多的时间和精力去考虑如何建模,已经开发过程中会遇到的各种问题,到那时这样的投入是完全值得的,相信这个系列的结束,大家有一个中肯的评价。

过去系统分析和系统设计都是分离的,正如我们国家“系统分析师” 和“系统设计师” 两种职称考试一样,这样割裂的结果导致,需求分析的结果无法直接进行设计编程,而能够进行编程运行的代码却扭曲需求,导致客户运行软件后才发现很多功能不是自己想要的,而且软件不能快速跟随需求变化。

从上图可以看出来,在项目初期,领域驱动设计是需要最高的努力值,付出的,但是随着项目复杂度增加,它的辛苦值并没有受到很大的波动,反之剩下两个,都是陡值。

 

1、过度耦合

业务初期,我们的功能大都非常简单,普通的CRUD就能满足,此时系统是清晰的。随着迭代的不断演化,业务逻辑变得越来越复杂,我们的系统也越来越冗杂。模块彼此关联,谁都很难说清模块的具体功能意图是啥。修改一个功能时,往往光回溯该功能需要的修改点就需要很长时间,更别提修改带来的不可预知的影响面。

 

 

订单服务接口中提供了查询、创建订单相关的接口,也提供了订单评价、支付、保险的接口。同时我们的表也是一个订单大表,包含了非常多字段。在我们维护代码时,牵一发而动全身,很可能只是想改下评价相关的功能,却影响到了创单核心路径。虽然我们可以通过测试保证功能完备性,但当我们在订单领域有大量需求同时并行开发时,改动重叠、恶性循环、疲于奔命修改各种问题。

上述问题,归根到底在于系统架构不清晰,划分出来的模块内聚度低、高耦合。

 

2、增加我们项目的业务价值

在开发中,什么是最宝贵的,当然是开发人员的时间效率最重要,然后还有就是沟通上,如果我们自己自定义的开发出一套代码,没有一系列的业务模块,这样的话,就是一个纯技术的项目,虽然本来领域专家也看不懂,但是如果我们仅仅专注于技术方面,而没有把业务人员的思想所映射到开发者中,那整个项目也只有可能仅仅是你成为这个领域专家,因为只有开发者当事人才能看懂。

这样随着时间的发展,随着开发者当事人的离职或者转向其他项目,本应该驻留在软件中的领域知识也就丢失了。在以后的任何修改代码中,新的开发人员虽然可以重新和领域专家进行思想的沟通,但是之前开发的代码已经无法撼动,这也就是为什么现在我们找工作的时候,不喜欢修改别人的代码,而且也会经常听到这个情况:尽量不要修改原来的代码,自己新建一个,嗯,这个更是要命了的。

试想一下,如果我们把注意力重点放到了业务上,这样就很清晰的进行迭代,而且也可以进一步的与领域专家进行对接。那我们的项目中都需要用到 DDD 领域驱动设计么?答案当时是否定的,如果满足以下几点,我建议还是需要使用 DDD 吧:

1、如果你的系统中有30~40个用户故事或者用例流的时候,软件的复杂性就出来了。

这里说明下用户故事和用例流:就比如一个商城,我们有普通用户,会员,公司管理员,企业合作伙伴,站长等等多个角色,当然还有其他的角色,每一个角色又会有一系列的操作,比如用户会浏览商品,下单,修改个人信息等等一系列的操作,这个就是用例流,这些角色所进行的全部操作,就是用户故事。

2、如果你的系统现在不是很复杂,但是以后会复杂。就比如我们的商城后台:本来是仅仅的对数据库表的CURD,但是在真正的用户(管理员)使用的时候,会发现在商品商家的时候,并不是很方便,需要用到价格规格表,或者发现商家信息已经实现商品的多对多分配(现在可能是一对多),那想想后期的修改是很庞大的。

3、如果你的系统所在的领域不是很清晰,就连领域专家也不是很清晰,需要在未来几年的慢慢讨论,变化中前进的时候。

 

3、贫血症和失忆症

相信大家都听过这两个名词,虽然听着不是很舒服,但是却天天在我们的手中被设计出来,如果你说不会,那好,请看下边两个情况是否在你的系统中出现(这里说明下,这些栗子都是我现在手中的项目):

1、你的领域对象(就是指实体类对象)是不是主要包含些共有的get 和 set,并且几乎没有业务逻辑?比如这样:

    public class BlogArticle
    {
        public int bID { get; set; }
        public string bsubmitter { get; set; }
        public string btitle { get; set; }
        public string bcategory { get; set; }
        public string bcontent { get; set; }
        public int btraffic { get; set; }
        public int bcommentNum { get; set; }
        public DateTime bUpdateTime { get; set; }
        public System.DateTime bCreateTime { get; set; }
        public string bRemark { get; set; }
    }

2、你的软件组件(就是指我们的方法定义)中是否经常会用到领域对象来实现一些业务逻辑,而且还是通过 get 和 set 的方式,直接将其抛给服务层或者应用层,甚至是直接抛到页面上。

        //添加一个车票
        public ActionResult AddTicket(FormCollection form)
        {
            var TitleHead = form["TitleHead"].ToString();
            var txtSubmitter = form["txtSubmitter"].ToString();
            var TicketChannelOperateJson = form["TicketChannelOperateJson"].ToString();

            try
            {
                if (!string.IsNullOrEmpty(TitleHead) && !string.IsNullOrEmpty(TicketChannelOperateJson) && !string.IsNullOrEmpty(txtSubmitter))
                {

                    Tickets tickets = new Tickets
                    {
                        Submiter = txtSubmitter,
                        SubmitDate = DateTime.Now,
                        SubmitData = TicketChannelOperateJson,
                        ActionType = Common.Enums.ActionType.Add,
                        ActionStatus = Common.Enums.ActionStatus.New,
                        Approver = "",
                        LastUpdateDate = DateTime.Now,
                        IsDelete = false,
                        IsCleanUp = false,
                        IsCheckIn = false,
                        TicketType = TicketType.Channel,
                    };

                    TicketsBLL ticketsBLL = new TicketsBLL();

                    bool flag = ticketsBLL.Add(tickets) > 0;
                }
            }
            catch (Exception e)
            {
                Logger.Log(e.InnerException + e.Message);
            }

        }

如果两个情况你都说的是 NO ,那么恭喜你,你的代码设计很健康,没有检查出来有贫血问题,如果你的回答中有一个 YES,那是不存在的,如果是两个YES,我们就请继续往下看吧。

4、内存模型和数据库模型保持一致

三层架构的最大问题在于:实际应用中人们喜欢把内存模型和数据库模型保持一致。三层架构的大部分问题都是从这里衍生出来的。

数据库模型的粒度如果很小,那么大量的表连接很快就会让数据库跑不动了。

如果数据库模型的粒度如果很大(这是大部分项目的选择),代码的质量(重用性、稳定性、扩展性)就很差。由于没有从业务的角度去仔细定义每一个对象,每个人会根据自己的需要建立各种QueryModel或ViewModel,慢慢地类会多到想哭。或如果不建立各种Model,强行重用DataModel的话,那么接口提供的内容往往绝大部分都不是你想要的。

内存模型与数据库模型保持一致并非天生的,这是有很多原因造成的:

它建模的简单性让初学者无法拒绝,由于经验主义,以至于多年以后已经没有勇气去摆脱了;

没有专门论述三层与建模的书籍;

ORM工具误导,与数据表结构一致内存结构方便建立映射关系;

示范代码的误导,错误把示范代码当成产品代码;

等等......

种种误导导致很多人工作很多年后依然未能找到正确的路,忽略了一个重要的核心(业务建模)环节(业务模型要与代码的数据结构保持一致),以至于让人产生无论你去到那个公司都是一样的错觉(在很多人的经历中,这的确是事实,包括我自己),但事实不应该是这样的。用户界面,领域模型,数据库它们应该具有同等的重要位置,领域模型在很多公司都是被忽略的角色。

由于目前的服务层职责是非常轻的,甚至有很多空壳的调用,所以平衡一下职责,把调用数据访问层的职责从业务逻辑层提升到服务层,需要的数据通过参数传递给业务逻辑层。这样,对于那些简单到无业务逻辑的CRUD就不需要去访问业务层了,直接调用数据访问层。我们就可以把业务逻辑与业务实体移到一块,然后把属于业务实体的逻辑迁移到实体类中。

上面很简单就把问题搞定了,但是实际迁移过程中,风险是非常大的,如果没有充分掌握重构知识,建议不要在正式产品代码上尝试,这个场景你一定遇到过,数据库模型设计好了,但是业务中需要用到新的属性,但是这个却不是数据模型的一部分,我们这个时候就只能修改数据库模型——实体类了。

 

二、贫血对象对我们做了什么

如果你上边的都已经看明白了,那我们就开始建我们的项目了,当然这里要说明下,以后会对项目进行修缮,前几章的代码可能很 low ,甚至是不好的,只是为了能说明问题。

1、新建一个 .net core web 项目

在指定的文件夹下,新建一个 Christ3D 解决方案,然后再新建一个 web 项目,具体过程相信大家都会了,如果不会,请会看第一个系列《系列教程一目录:.netcore+vue 前后端分离

 

2、在Models 文件夹中,新增我们的领域对象 Customer.cs 和持久化虚拟类 CustomerDao.cs

    /// <summary>
    /// Customer 领域对象
    /// </summary>
    public class Customer
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public DateTime BirthDate { get; set; }
    }


    /// <summary>
    /// 领域对象持久化层
    /// </summary>
    public class CustomerDao
    {
        public static Customer GetCustomer(string id)
        {
            return new Customer() { Id = "1", Name = "Christ", Email = "Christ@123.com" };
        }


        public static string SaveCustomer(Customer customer)
        {
            return "保存成功";
        }
    }

 

3、在默认的 HomeController.cs 控制器中,添加保存顾客方法 saveCustomer()

/// <summary>
/// 保存顾客方法:add & update
/// </summary>
/// <param name="id"></param>
/// <param name="name"></param>
/// <param name="email"></param>
/// <param name="birthDate"></param>
public void saveCustomer(string id, string name, string email, string birthDate)
{
    Customer customer = CustomerDao.GetCustomer(id);
    if (customer == null)
    {
        customer = new Customer();
        customer.Id = id;
    }

    if (name != null)
    {
        customer.Name = name;
    }
    if (email != null)
    {
        customer.Email = email;
    }

    //...还有其他属性

    CustomerDao.SaveCustomer(customer);
}

 

到这里,这就是一个领域 —— 对顾客领域对象进行保存,这个功能还是很强大的,不管一个 Customer 是新增还是更新,甚至是不管名字换了,还是邮件重新申请了,都可以在这里体现,相信这样的方法我们都写过,至少我是这样。

这个方法真的很强大么,这里我们看一下,我们并不知道 saveCustomer() 的具体的应用场景(是后台管理,还是前台用户自己操作,还是更新手机号等等),经过几周或者几年后,我们完全不知道当时创建这个方法的本来目的和意图,当然你会说可以通过注释的方法,我表示是反对的,我们对程序的设计,不能依赖于注释,这是很可怕的。

还有,这些方法嵌套在一起,我们不能对其进行单元测试,都不用说细节,可能数据库的约束就能对这个方法造成崩溃:比如时间字段,比如字符串长度过长,比如不能为空等,而且因为有很多地方调用这个看似功能强大的基方法,我们需要一个一个的往上游去研究,看看到底是哪个地方,哪个方法,哪个业务逻辑调用了,可能需要几个小时的时间,甚至更久。

总结来说,上边的 saveCustomer() 方法存在三个弊端:

1、saveCustomer() 方法的业务意图不明确,当时我们为了贪图功能的强大,把注意力都放到了技术上,从而忽略了业务核心。

2、saveCustomer() 方法的实现本来就增加了潜在的复杂性,虽然看着强大了,可是复杂度却直线向上。

3、Customer 顾客领域对象根本不是对象,充其量就是一个数据持有者,仅仅是把我们的数据从持久化的数据库中拿出来到内容的一个工具。

 那既然如此,我们需要怎么办呢,没错,就是我们平时使用到的分层,一个专注领域业务的分层 —— DDD领域驱动设计,就出现了。

 

三、一切皆从领域开始 —— Domain

还记得下边这个图么,这个是我上一篇文章中提到的《领域驱动设计架构图》,我个人表示,这个图已经很贴切和较为详细的表示了DDD领域驱动设计是整体框架图,大家从整体的箭头走向就能看清楚(谁指向谁,表示前者依赖后者后者实现前者),其中的内容我们都会说到,今天先简单的把这个整体框架搭起来,至少先从什么的DDD框架讲起来——当然也就是从被依赖最多的领域层开始。

从上边的文章中,我们知道了,在软件开发中,我们已经把重点放到领域业务上,在上边的 saveCustomer() 的方法中,所有的逻辑和用例都一股脑的放在一起,完全背离了这个思想,所以,那我们如果想要通过领域设计的思路来创建,会是怎么样的呢,请往下看。

1、定义领域对象 Customer.cs(值对象/聚合/根,以后会说)

在解决方案中,新建 .net core 类库 Christ3D.Domain ,作为我们的领域层(这是一个臃肿的领域层,以后我们会把领域核心给抽象出来,现在简化是为了说明),然后在该层下,新建 Models 文件夹,存放我们以后的全部领域对象,我们的专业领域设计,都是基于领域对象为基础。

老张:这里并没有增加业务逻辑,以后会说明

 /// <summary>
    /// 定义领域对象 Customer
    /// </summary>
    public class Customer
    {
        protected Customer() { }
        public Customer(Guid id, string name, string email, DateTime birthDate)
        {
            Id = id;
            Name = name;
            Email = email;
            BirthDate = birthDate;
        }

        public Guid Id { get; private set; }
        public string Name { get; private set; }
        public string Email { get; private set; }
        public DateTime BirthDate { get; private set; }
    }

 

2、定义泛型接口 IRepository.cs

这里说下为什么开发中都需要接口层:

在层级结构中,上层模块调用下层模块提供的服务,这里就会存在一种依赖关系,Rebort C. Martin提出了依赖倒置原则大致是如下:

上层模块不应该依赖于下层模块,两者都应该依赖于抽象;

抽象不应该依赖于实现,实现应该依赖于抽象;

这是一个面向接口编程的思想。

在我们的领域层下,新建 Interfaces 文件夹,然后添加泛型接口

在我们专注的领域业务中,我们只需要定义该领域Customer 的相关用例即可(就比如如何CURD,如何发邮件等等,这些都是用户角色Customer的用例流),而不用去关心到底是如何通过哪种技术来实现的,那种ORM去持久化的,这就是领域设计的核心,当然现在有很多小伙伴还是喜欢直接把接口和实现放在一起,也无可厚非,但是不符合DDD领域设计的思想。

可能这个时候你会说,领域层,定义接口和实现方法放在一起也可以嘛,现在我们是看不出来效果的,以后我们会在这里说到领域驱动,领域通知,事件驱动等等知识点的时候,你就会发现,在Domain层来对接口进行实现是那么格格不入,没关系慢慢来~~~

    /// <summary>
    /// 定义泛型仓储接口,并继承IDisposable,显式释放资源
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public interface IRepository<TEntity> : IDisposable where TEntity : class
    {
        /// <summary>
        /// 添加
        /// </summary>
        /// <param name="obj"></param>
        void Add(TEntity obj);
        /// <summary>
        /// 根据id获取对象
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        TEntity GetById(Guid id);
        /// <summary>
        /// 获取列表
        /// </summary>
        /// <returns></returns>
        IQueryable<TEntity> GetAll();
        /// <summary>
        /// 根据对象进行更新
        /// </summary>
        /// <param name="obj"></param>
        void Update(TEntity obj);
        /// <summary>
        /// 根据id删除
        /// </summary>
        /// <param name="id"></param>
        void Remove(Guid id);
        /// <summary>
        /// 保存
        /// </summary>
        /// <returns></returns>
        int SaveChanges();
    }

 

3、定义 Customer 领域接口 ICustomerRepository.cs

 我们以后的每一个子领域中特有的接口,还是需要定义的,并且继承自我们的泛型仓储接口

    /// <summary>
    /// ICustomerRepository 接口
    /// 注意,这里我们用到的业务对象,是领域对象
    /// </summary>
    public interface ICustomerRepository : IRepository<Customer>
    {
        //一些Customer独有的接口
 Customer GetByEmail(string email);
}

好了,这个时候我们的最最最简单的领域层就搭建好了,里边有我们的子领域 Customer的相关接口实现,整体结构是这样的:

 

 

四、基础层对领域进行实现 —— Infrastructure 

 上边咱们定义了 Domian 领域层,这是一个接口层,那我们必须来实现它们,大家可以再往上看那个DDD领域驱动设计架构图,你应该能找到,是谁对领域层进行了实现,答案当然是基础设施层。

我们在解决方案下,新建一个 Christ3D.Infrastruct.Data 类库,你一定会问,为什么不直接是 Christ3D.Infrastruct ,反而在后边还需要多一个 .Data 后缀呢,这里要说的就是,在这个基础设施层中,会有很多很多的内容,比如验证层,IoC层,事务,工作单元等等,以后都会说到,至少从名字上你也能明白——基础设施,这些基础东西是不能放在领域层的,这是肯定的,因为我们关心的是业务领域,这个不是我们的业务,也不会是下文的应用层,至于为什么,你可以先想一想,数据验证和AOP这些为何不放在应用层

1、新建泛型仓储 Repository 

Repository继承了IRepository接口,这里我们先不写具体的实现,我们先定义好方法体,以后再慢慢填上,大家还记得如何快速的实现接口吧,Ctrl+.  可能有的小伙伴没有这个功能,那就只能手动了。

 

 

    /// <summary>
    /// 泛型仓储,实现泛型仓储接口
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        public void Add(TEntity obj)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public IQueryable<TEntity> GetAll()
        {
            throw new NotImplementedException();
        }

        public TEntity GetById(Guid id)
        {
            throw new NotImplementedException();
        }

        public void Remove(Guid id)
        {
            throw new NotImplementedException();
        }

        public int SaveChanges()
        {
            throw new NotImplementedException();
        }

        public void Update(TEntity obj)
        {
            throw new NotImplementedException();
        }
    }

 

2、实现子领域Customer 仓储

  /// <summary>
    /// Customer仓储,操作对象还是领域对象
    /// </summary>
    public class CustomerRepository : Repository<Customer>, ICustomerRepository
    {
        //对特例接口进行实现
        public Customer GetByEmail(string email)
        {
            throw new System.NotImplementedException();
        }
    }

最终结构

 

相信大家看到这里,基本还是很轻松的,我们在领域层,把业务定义清楚,把领域对象设计好,然后在基础层对其进行实现,是一个很好的过程,这么看还是可以的。

但是一定会有小伙伴会看不惯这么写,他会说,领域设计定义接口我懂,那就定义,定义实现也可以,比如用 EFCore 或者其他 ORM 框架,那这样直接在展示层调用不就好了,为啥还需要单拿出来一个应用 Application 层呢(等同于我们之前的 Service 层),到时候肯定又多出来一套接口和实现的过程,麻烦不麻烦?

我在上一个系列教程中,本来也是这么尝试使用DDD领域驱动设计,可以中间没有看《实现领域驱动设计》这本书,导致出现了漏洞,也被各种小伙伴吐槽,这个系列就再证明一下吧,具体有什么好处,或者说为什么要在基础设施层之上,再增加一个应用层(也就是我们的 Service 层),这里先不说,下边请继续看。

 

五、定义系统的业务功能 —— Application

如果Repository 应用在应用层,会出现什么情况:这样就致使应用层和基础层(我把数据持久化放在基础层了)通信,忽略了最重要的领域层,领域层在其中起到的作用最多也就是传递一个非常贫血的领域模型,然后通过 Repository 进行“CRUD”,这样的结果是,应用层不变成所谓的 BLL(常说的业务逻辑层)才怪,另外,因为业务逻辑都放在应用层了,领域模型也变得更加贫血。 

Application为应用层(也就是我们常说的 Service 层),定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其它系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。

1、视图模型——Rich 领域模型(DTO以后说到)

在文章的最后,咱们再回顾下文章开头说的贫血对象模型,相信你应该还有印象,这个就是对刚刚上边这个问题最好的回答,如果我们直接把展示层对接到了基层设施层,那我们势必需要用到领域模型来操作,甚至是对接到视图里,不仅如此,我们还需要验证操作,传值操作等等,那我们又把领域对象模型过多的写到了业务逻辑里去,嗯,这个就不是DDD领域驱动设计了,所以我们需要一个应用层,对外进行数据接口的提供,这里要强调一点,千万不要把应用层最后写满了业务逻辑,业务应该在领域层!!!

在项目根路径下,新建 Christ3D.Application 类库,作为我们的应用层,然后新建 ViewModels 文件夹,用来存放我们的基于UI 的视图模型,它是如何来的,这个下边说到。

 

 /// <summary>
    /// 子领域Customer的视图模型
    /// </summary>
    public class CustomerViewModel
    {
        [Key]
        public Guid Id { get; set; }

        [Required(ErrorMessage = "The Name is Required")]
        [MinLength(2)]
        [MaxLength(100)]
        [DisplayName("Name")]
        public string Name { get; set; }

        [Required(ErrorMessage = "The E-mail is Required")]
        [EmailAddress]
        [DisplayName("E-mail")]
        public string Email { get; set; }

        [Required(ErrorMessage = "The BirthDate is Required")]
        [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
        [DataType(DataType.Date, ErrorMessage = "Data em formato inválido")]
        [DisplayName("Birth Date")]
        public DateTime BirthDate { get; set; }
    }

这里仅仅是增加了特性,更多的业务逻辑还是在 领域层 来实现的。

 

2、定义应用服务接口 ICustomerAppService,依赖抽象思想

 在我们的应用层下,新建 Interfaces 文件夹,用来存放我们的对外服务接口,然后添加 Customer服务接口类,这里要说明下,在应用层对外接口中,我们就不需要定义泛型基类了,因为已经没有必要,甚至是无法抽象的,

    /// <summary>
    /// 定义 ICustomerAppService 服务接口
    /// 并继承IDisposable,显式释放资源
    /// 注意这里我们使用的对象,是视图对象模型
    /// </summary>
    public interface ICustomerAppService : IDisposable
    {
        void Register(CustomerViewModel customerViewModel);
        IEnumerable<CustomerViewModel> GetAll();
        CustomerViewModel GetById(Guid id);
        void Update(CustomerViewModel customerViewModel);
        void Remove(Guid id);
    }

 

3、实现应用服务接口 CustomerAppService.cs ,对接基层设施层

 在我们的应用层下,新建 Services 文件夹,用来存放我们对服务接口的实现类

    /// <summary>
    /// CustomerAppService 服务接口实现类,继承 服务接口
    /// 通过 DTO 实现视图模型和领域模型的关系处理
    /// 作为调度者,协调领域层和基础层,
    /// 这里只是做一个面向用户用例的服务接口,不包含业务规则或者知识
    /// </summary>
    public class CustomerAppService : ICustomerAppService
    {
        private readonly ICustomerRepository _customerRepository;

        public CustomerAppService(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }

        public IEnumerable<CustomerViewModel> GetAll()
        {
            return null;
            //return _customerRepository.GetAll().ProjectTo<CustomerViewModel>();
        }

        public CustomerViewModel GetById(Guid id)
        {
            return null;
            //return _mapper.Map<CustomerViewModel>(_customerRepository.GetById(id));
        }

        public void Register(CustomerViewModel customerViewModel)
        {
            //var registerCommand = _mapper.Map<RegisterNewCustomerCommand>(customerViewModel);
        }

        public void Update(CustomerViewModel customerViewModel)
        {
            //var updateCommand = _mapper.Map<UpdateCustomerCommand>(customerViewModel);
        }

        public void Remove(Guid id)
        {
            //var removeCommand = new RemoveCustomerCommand(id);
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
        }
    }

目前这里还没有具体使用基础层的仓储,为什么呢,因为应用层是面向视图对象模型,不涉及到业务,而基础设施层和领域层是基于 领域对象模型,面向业务的,所以我们需要用到 DTO ,这一块以后我们会说到。

 

六、结语

 好啦,今天的讲解基本就到这里了,到目前为止,仅仅是实现了DDD领域驱动设计的第一个 D 领域模型,还没有说到驱动的概念,通过经典的DDD四层,大家应该也了解了各层的作用,这里有简单的三个问题,不知道你是否已经真的看懂了,如果都能回答上来,恭喜!如果不是很确定,那抱歉,还需要再看看,或者查资料看书,或者来群里咨询我吧。

1、什么是贫血对象模型?

2、我们的业务接口和业务实现,分别在哪一层?( Answer:领域层和基础设施层 )

3、为什么还需要定义一个应用层,而不是直接在应用层(Service层)对接基础层(Repository层)?

好啦,周四我们会继续推进,DDD 领域对象设计是如何实现 领域、子域、界限上下文的

 

七、Github & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD 

 

posted @ 2018-10-23 13:21 老张的哲学 阅读(...) 评论(...) 编辑 收藏
作者:老张的哲学
好好学习,天天向上