如何运用DDD - 领域服务

概述

本文将介绍领域驱动设计(DDD)战术模式中另外一个很是重要的概念 - 领域服务。在前面两篇博文中,咱们已经学习到了什么是值对象和实体,而且可以比较清晰的定位它们自身的行为。可是在某些时候,你会发现某一些业务行为好像不容易落到单个实体或者值对象身上,而且会为放置这一部分业务逻辑而困惑。此时,你可能须要一个领域服务来完成操做。git

那么,到底什么是领域服务呢?怎么发现领域中的领域服务呢?领域服务和传统的应用服务又有什么区别呢?本文将从不一样的角度来带你们从新认识一下“领域服务”这个概念,而且给出相应的代码片断(本教程的代码片断都使用的是C#,后期的实战项目也是基于 DotNet Core 平台)。github

什么是领域服务

在开始以前,仍是说一点题外话吧:若是你们读过这个系列的前几篇文章,可能都会发现该系列的风格都是从原著的解析开始,而后结合了自身的一些案例和实际场景来为你们解读领域驱动中的一些概念。我也不知道这样的写做方式能不能让你们更清楚的理解,因此若是你们有什么建议的话能够在评论区留言,我必定会认真的听取你们的意见和建议。c#

在文章中,我会尽量避免各种名称的简写(好比事件溯源,有些同窗喜欢简写为ES),虽然简写有时候确实会很方便,可是会让人与人之间的沟通成本无形的增大,因此在个人博文中只要能不用简写的地方我都不会使用简写。架构

另外还有一点就是,可能前期属于概念性的东西比较多,因此就没有现成的github代码供你们参考,很少你们不用担忧,在完成这几回的概念学习以后咱们就开始咱们的code time(●'◡'●)。框架

回到正题吧,什么是领域服务呢?看看原著原著《领域驱动设计:软件核心复杂性应对之道》中所说起到的领域服务的概念:性能

在某些状况下,最清楚、最实用的设计会包含一些特殊的操做,这些操做从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其天然地在模型中引入一种新的元素,这就是Service(服务)。
当领域中的某个要的过程或转换操做不属于实体或值对象的天然职责时,应该在模型中添加一个做为独立接口的操做,并将其声明为Service.定义接口时要使用模型语言,并确保操做名称是UBIQUITOUS LANGUAGE中的术语。此外,应该将Service定义为无状态的。学习

李姐万岁

额。。。。“李姐万岁”。这个概念很差理解的缘由是由于:首先它假设咱们寻找到了领域中一些“包含特殊的操做”,也就是说咱们在此时已经具有了划分领域中各类对象以及其对应行为的能力,而后咱们再来考虑提取出这个传说中的“service”(也就是咱们本次的主题领域服务)。而每每现实则是,做为一个初学者,咱们并不能合理的抽象出各个对象,而且也没有一个好的案例来进行体验性的思考。因此在读这个概念的时候就很迷惑,咱们没法找到概念中的“这些操做”是什么东西,也就更不能理解这个Service是什么了。设计

“在本身的私人飞机里面玩儿电子游戏是什么感受呢?   呃.....好像前提是我得有钱买一架飞机吧?”

从实际场景下手

我思考了不少种方法来表述“领域服务”,可是想了半天好像都不太容易能让人理解。因此该篇博文采用先从案例入手的思路,但愿你们能从这个案例可以理解出领域服务的用处。3d

来回顾一下上一篇文章 《如何运用DDD - 实体》 中咱们所提炼出来的一个实体对象:

public class Itinerary
{
    public int ID { get; set; }

    public List<Person> Participants { get; set; }

    public List<Address> Places { get; set; } 

    public ItineraryNote  Note { get; set; } 

    public ItineraryTime TripTime { get; set; }

    public ItineraryStatus Status { get; set; }

    //ctor

    public void ChangeNote(string content)
    {
        Note = new ItineraryNote(content);
    }
}

该实体对象代表了一次旅行的行程。目前做为示例,咱们仅仅知道了在该领域中咱们容许修改行程的备注信息,因此咱们在上一篇文章中为它赋予了修改备注的一个行为。

根据项目的进展,咱们如今捕获到了另外一个需求:若是行程没有结束,用户访问到该行程,系统会根据用户目前所在的地点为用户推荐附近好吃的美食。

这是一个很是人性化以及好用的功能,也是该产品能够和其余同类型的产品系统竞争的优点。因此咱们理应将它放置于领域来考虑。从该功能需求的描述来看,咱们要作的是一个推荐美食的行为。可是让咱们矛盾的是,推荐美食这一个动做,咱们应该将它归属于谁呢? 给旅程?让旅程实体来推荐美食? 很显然,你并不会这么作。旅程仅仅关心的是本次旅行的基本信息,地点人物时间等,咱们不会将推荐美食这一个动做给它,让它成为一个万能的机器。

来回顾一下上面所说的概念:“在某些状况下,最清楚、最实用的设计会包含一些特殊的操做,这些操做从概念上讲不属于任何对象。” 仔细读几遍,纳尼?这不是说的就是这个状况吗? 在如今这个状况下,咱们出现了一个推荐美食的操做,可是它却不属于任何对象。

当走到这一步时,可能咱们已经有一点理解领域服务了。接下来,继续往下走。如今,咱们已经明白了,可能咱们须要一个Service来处理这一个操做。尝试着来创建一个 RecommendFoodsService

public class RecommendFoodsService
{
    public List<RecommendFoodInfo> RecommendFoods(Itinerary currentItinerary)
    {
        //todo
    }
}

在该领域服务中,有一个RecommedFoods的方法,它经过获取到当前的旅程,返回一个推荐美食的列表。它内部的实现方法多是这样的:(在这里咱们假设ItineraryPlaces中的最后一个地点就是咱们的当前地点,并且咱们已经有一个叫作餐厅 Restaurant 的实体,该实体提供了有关餐馆的一系列信息和行为。固然,你能够本身尝试创建餐厅这样一个实体,以便加深对实体章节的印象)

public class RecommendFoodsService
{
    public List<FoodInfo> RecommendFoods(Itinerary currentItinerary)
    {
        var recommendFoods = new List<FoodInfo>();

        //Get Last Address
        int lastCountIndex = currentItinerary.Places.Count -1;
        var currentAddress = currentItinerary.Places[lastCountIndex];

        var nearbyRestaurants = Restaurants.Where(s=> s.Address.isNearby(currentAddress)).ToList();

        foreach(var restaurant in nearbyRestaurants)
        {
            var food = restaurant.GetRankNoOneFood();

            recommendFoods.Add(new FoodInfo(food,restaurant.Address));
        }

        return recommendFoods;
    }
}

OK,到目前咱们已经完成了一个演示版本的领域服务,在该服务中,咱们经过获取到当前的旅程的位置,根据该位置,从系统中存在的餐馆集合中找到了距离该位置最近的餐厅,而后再将这些餐厅中排名评价最好的一道菜推荐给用户。

来看看上面的行为中出现了哪些东西,首先是咱们的行程,而后是餐馆。经过合理的处理这两个实体之间的关系,咱们完成了咱们的一系列操做,而且返回了一个美食信息的集合(在这里美食信息咱们定义为了一个值对象)。要注意,虽然咱们里面包含了几个实体和几个值对象,以及使用了他们之间的不一样行为,可是从推荐美食这一个行为来看,他们其实是一个总体,是密不可分的处理逻辑(敲重点!!!)。

更贴近现实

上面的版本咱们将他做为一个演示版原本定义,是由于在实际的状况中,咱们每每是经过存储库(Repository,有关该内容的介绍会在后期文章中介绍)来获取到实体集合的信息的,就如同上面代码中的Restaurants。有可能更贴近于咱们现实中的代码是相似于下面这样,不过咱们如今能够不用考虑这种写法,由于里面涉及到了存储库(仓储 Repository) 和 聚合根(AggregateRoot) 的概念,而如今咱们只须要理解好领域服务就行了。

public List<FoodInfo> RecommendFoods(int currentItineraryID)
    {
        var recommendFoods = new List<FoodInfo>();

        //Get Last Address
        var currentItinerary = itineraryRepository.Get(currentItineraryID);
        int lastCountIndex = currentItinerary.Places.Count -1;
        var currentAddress = currentItinerary.Places[lastCountIndex];

        var nearbyRestaurants = restaurantRepository.GetNearbyRestaurant(currentAddress);

        foreach(var restaurant in nearbyRestaurants)
        {
            var food = restaurant.GetRankNoOneFood();

            recommendFoods.Add(new FoodInfo(food,restaurant.Address));
        }

        return recommendFoods;
    }

来吧,根据咱们如今所理解和发现的内容,来看一下领域服务的一些特色:

  • 领域服务处理的是领域中的对象,好比实体、值对象等
  • 领域服务是负责对领域中一系列对象的编排处理
  • 当咱们发现一个操做没法赋予一个实体或者值对象,且该操做又对业务流程很重要时,咱们每每须要使用领域服务
  • 领域服务中的操做,从领域的角度来看,它是一个总体

若是你在进行下面的操做时,可能证实你须要一个领域服务:

  • 经过A和B,获得一个C。
  • A须要一个繁琐的内部策略才能获得一个结果B。

(ps: A,B,C指的是领域对象中的值对象或者实体)

领域服务VS应用服务

其实在使用领域驱动中,还有一个服务叫作应用服务,应用服务是划分在应用层的服务。而每每都是由于叫作服务,因此你们很难区分它与领域服务有什么区别,最终的结果就是要么形成应用服务很庞大(全部的逻辑编排都在该层处理了),要么就是应用服务很薄弱(就一句调用领域服务的代码)。无独有偶,当应用服务开始混乱时,领域服务也会变得混乱,由于原有领域服务的逻辑你可能给了应用服务,而应用服务的逻辑又给了领域服务。

在比较二者以前,来看一看传统领域驱动设计为你们提供的四层架构示意图:

DDD四层

从图中能够看到,应用层保持了对领域层的引用关系,也就是说在应用层中,能够访问到领域对象。因此让应用层也具有了编排领域对象的能力。这一点和咱们的领域对象的特征相同了,因此在不少时候,你们对应用服务和领域服务的区分难度就加大了。

关于应用服务,由于在原著中我没有找到对应的关键语句,因此选取了网上的一些结论供你们参考:

应用服务是用来表达用例和用户故事(User Story)的主要手段。
应用层经过应用服务接口来暴露系统的所有功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它自己只负责处理业务用例的执行顺序以及结果的拼装.

从上面的结论中咱们大概能够知道,应用服务是为了让应用可以运用而且支撑对外的用户可以访问领域对象和执行领域逻辑的一层。就比如在dotnetoore中,用户能够经过访问咱们定义的controller来访问咱们的业务对象,而且还能够经过controller暴露出来的接口来执行业务逻辑。

所以,咱们能够将应用服务考虑为执行业务逻辑的一个中介(可能这样定义也不太好),它没有涉及到核心领域的任何逻辑过程,它只负责了一些的验证,构件的支持等(好比日志,性能监控等)。

扩展上面的需求

在上面识别领域服务中,咱们已经捕获到了这样一个需求:“若是行程没有结束,用户访问到该行程,系统会根据用户目前所在的地点为用户推荐附近好吃的美食。” 后来需求又增长了一项:“咱们能够用短信的方式将美食通知给客户。”

那么考虑这样一个需求,咱们该把短信通知这一个功能实现放在哪儿呢?或者说将发短信这个行为操做放在哪儿呢?咱们来考虑一下将他放置在领域服务中:

public class RecommendFoodsService
{
    public List<FoodInfo> RecommendFoods(Itinerary currentItinerary)
    {
        var recommendFoods = new List<FoodInfo>();

        //Get Last Address
        int lastCountIndex = currentItinerary.Places.Count -1;
        var currentAddress = currentItinerary.Places[lastCountIndex];

        var nearbyRestaurants = Restaurants.Where(s=> s.Address.isNearby(currentAddress)).ToList();

        foreach(var restaurant in nearbyRestaurants)
        {
            var food = restaurant.GetRankNoOneFood();

            recommendFoods.Add(new FoodInfo(food,restaurant.Address));
        }

        //在这里添加短信发送?
        SmsUtil.Send(currentItinerary.Participants,recommendFoods);

        return recommendFoods;
    }
}

咱们在原有代码的基础上,添加了一行代码,为其实现短信通知功能,如今这样已经符合咱们的需求了。可是!!!!将短信通知放置在这里好吗?为解开这个问题,咱们须要考虑:“短信发送是我领域提炼出来的行为吗?”,“若是没有这个行为,对业务逻辑有什么影响?”

来想想,发短信是领域提炼出来的吗? 咱们一直都在关心有关旅程的问题,很显然旅程中的各类才是咱们主要关心的对象。那么发短信就不是咱们所提炼出来的东西,它只是须要咱们附带的支持功能罢了。

那么若是没有这个行为,对业务逻辑有什么影响呢? 它会不会影响我完成美食推荐这个行为? 很显然,不会! 还记得咱们在上文说的一个领域服务的特色吗:领域服务中的操做,从领域的角度来看,它是一个总体。 若是总体中的一部分丧失它就不能完成业务了。那么在如今这个推荐美食的业务中,若是把餐厅的一部分拿掉会是什么样子呢?OMG,这个服务已经废了,它失去了已有的功能。那若是把短信发送拿掉呢?好像没有一点点影响。

那么这个短信发送,到底放在哪儿呢? 应用服务!!!!!

public class ItineraryApplicationService 
{
    public string RecommendFoods(int currentItineraryID)
    {
        Logger.Log("执行推荐美食业务");

        var participants = itineraryRepository.Getparticipants(currentItineraryID);
        var foods = RecommendFoodsService.RecommendFoods(currentItineraryID);

        SmsUtil.Send(foods);

        return foods.toJson();
    }
}

咱们在应用层定义了一个叫作ItineraryApplicationService的应用服务,它对外提供了一个RecommendFoods的接口,客户端(App,网页等)能够透过该API来完成推荐美食这一系列的操做。推荐美食的行为咱们已经封装在了领域服务中,应用服务根本不须要知道内部的逻辑就能够完成操做,这也验证了咱们上面说的一点:从领域的角度看,领域服务是一个总体

最多见的认证受权是领域服务吗

就通常的应用来讲,认证受权是应用服务。为何呢?由于它每每只是给你提供了维持系统容许的基础功能,而并不是你领域执行的必须。也许,这还很差理解,那么咱们就来尝试一下将它定义为领域服务来看一看。考虑改为那个发短信的例子,咱们实现了一个错误版本的领域服务,那么如今咱们把领域服务的发短信替换为身份验证代码,而后放置在方法块最前面。来吧?继续回答上面的问题,他们是一个总体吗?若是剥离了这个代码,对行为有什么影响? 慢慢的你就会将它从领域服务中拿出来。
可是假如你正在实现一个组织权限软件,它可能会被定义在领域之中。由于你的领域就是认证的一系列操做,你须要认真的去思考它,一旦失去了认证的代码可能你的应用就没法提供正常的功能。

使用领域服务

你己经和领域专家谈论过涉及多个实体的领域概念了,但你不肯定哪一个实体“拥有”行为。看起来该行为并不属于任何一个实体,但当你尝试将该行为强制适配到实体中的任何一个时,处理起来就会有点棘手了。这一思惟模式就是须要领域服务的强烈迹象。[嘘,这句话是我copy的。(*^__^*) ]

不要过多的使用领域服务

是否是只有领域服务才能调度值对象和实体等领域对象呢? 固然不是,应用服务也能够。
这也是一个你们常见的问题:将全部实体、值对象、仓储都经过领域服务来编排完成业务逻辑。从而使得应用服务层很是的薄,每每只有一行调用领域服务的代码(日志,性能等代码经过一些现有框架自动完成)。

尝试将部分调度权限分配给应用服务,它不会影响你的领域代码可读性,反而会使得阅读更加清晰。当你发现你的逻辑编排只是调用实体或值对象之间的行为,而没有构成一个完整的领域业务行为的时候(好比有一个Api表示了获取一次旅行地点距离的功能,你能够不用将该功能考虑为领域服务,在应用服务中经过传入的ID,在仓储中获取本次旅行的行程地址,而后交给系统中的距离转换功能计算出距离,而后返回给客户端),请考虑将它设置为应用服务。

不要将过多的行为都给了领域服务

为何会这样说呢?若是你发如今你创建的领域模型中,实体和值对象的行为只是零星一点,而实体和值对象实现行为操做的动做都是经过领域服务来完成的。那么,你也许用错了领域服务,去从新认识你所识别出的实体和值对象,为它们赋予他们自身的行为,删除这些错误的领域服务。

总结

本次咱们介绍了领域驱动设计战术模式中的领域服务。同时也对比了领域服务和应用服务,该部份内容可能介绍的还不是太完整,但愿你们能从例子中理解二者之间的差别,后期若是有时间的话会为你们写一篇博文专门来区别领域服务和应用服务。在讲解的过程当中,咱们还涉及到了一切战术模式中的其余概念,好比Repository和AggregateRoot,这两个概念将在后期的文章中为你们带来介绍。

  • 15楼的评论中 @todo 朋友给了一个很好的建议,是当时写做的时候忽略的问题。

 
 
 

小彩蛋

强烈给你们推荐如今正在上映的一部动漫电影 《若能与你共乘海浪之上》。喜欢动漫的同窗可不要错过哦。
《若能与你共乘海浪之上》

相关文章
相关标签/搜索