上一章讲解了软件设计中主要用到的三个设计模型,本节讲解三个服务。等咱们这次都讲完了再最后进行一次归纳,即:系统开发流程中的三模型、软件设计中的三模型和三个服务,我习惯管这个叫3*3*3。看完了您就会知道我为什么常说软件设计这活是朴素的,没那么多弯弯绕,只是因为我们在学习过程中没有做思考和归纳。设计模式的那四个哥们儿不也是根据其经验总结出了流传至今且经久不衰的23个模式(其实常用的也没几个)以及6个原则。这里还是要再多说两句,23个设计模式其实属于代码设计阶段,应该是每个软件工程师必须掌握的一门技术。但,如果是初学者千万别刻意在产品中去用,强拧的瓜不甜,用完了搞不好还被领导骂你傻缺。我个人写代码时间久了,所以遇到一些典型的场景会直接使用某个模式,非典型的一般是在重构时决定是否有合适的模式供使用。另外,还有一些企业级软件的设计模式也需要去学习的,比如:熔断器、IoC、工作单元等。

  书归正传,一提到“服务”,您肯定会想:这简单啊,一个类里面没有字段只有方法就可以称其为服务。这样的定义太大且模糊,我们既然要学习设计就把这个东西整细了。也就是要总结一下到底有哪些服务,每个服务的作用是什么,应用于什么场景,这才叫系统化学习。首先一点,服务的定义说白了的确就是“只有方法没有字段的类”,这是使用服务的一种常用模式。也还有一些特别的,比如计数器服务类里面可能有“AtomicLong”类型的字段标识计数值。一般来说,服务顶好是不要包含字段且单例的,即使加了也尽量从技术角度做好并发访问控制;第二,好的服务每个接口都可以提供一个完整的功能;第三,服务主要是用于提供特定的功能,其代码一般是以面向过程为主。

  上面对服务进行了一个白话版的定义,精确度略显不足,不过本系列文章也不是站在高大上的角度来教化读者的。谈完了定义再说说服务的类型,软件设计过程中主要包含三类服务:领域服务、应用服务和数据服务,具体如下图所示。

戏说领域驱动设计(十二)——服务-LMLPHP

一、应用服务

  如果您学习过软件工程这本书,会发现在讨论设计相关的知识的时总是提一个词“控制类”。遥想当年,我在上这门课的时候对好多的词都不懂,比如“数据字典”、“控制类”、“用例”等,听着就不明觉厉,但实际上对这些东西完全没有什么概念,不过考试及格罢了。工作几年后,做了一堆系统,还是不太明白所谓的“控制类”到底是何方神圣。这个事儿说起来挺玄幻的,再后来就突然“顿悟”了,也可能是看资料看得多了突然有所感应。接触过DDD的您肯定知道有一个概念叫“应用服务”,再不济面向过程的代码您总会写过吧?里面那个“service”就是应用服务,也就是所谓的“控制类”。

  说到这儿您肯定明白了应用服务的作用了吧?后面我们会详细说明,在这里面只给出一个概念性的解释。应用服务的主要目的是控制一个事务内的(加紧拿小本本儿记下来,重点)业务流程的运转,先干啥后干啥全靠它来搞,是业务的入口。从UML的角度来看,一般是一个用例对应一个控制类的接口。我们在设计领域模型的时候限制较多比如不能访问基础设施,应用服务没这个限制,几乎可以随便玩儿,可能最重要的且强制性的要求就是只能访问其它包的应用服务,不能再向内进行入侵。需要注意的是不论您是用面向过程设计还是面向对象设计,控制类的代码永远是面向过程的。除了访问控制,应用服务的使用和设计还有许多的规范比如日志、返回值、异常处理等,后面会细聊。

二、数据服务

  数据服务就是DAO,用于执行数据的持久化与反持久化,这东西全宇宙都知道,也没什么可讲的。需要注意的是DAO不仅仅是用于MySQL这种关系型数据库,涉及其它非关系型如Redis、MongoDB的操作都需要在DAO内搞定,别一会儿放应用服务内,一会儿放DAO内。既然说到这儿了,我给您展示一下如何在DAO内同时操作MySQL和Redis。我这里的DAO基于MyBatis框架,使用了接口来与配置文件对应。上面说了,Redis操作也算是DAO内的东西,但现在的DAO是个接口,不能写代码的,so……ladies and gentlemen,请看示例。

public interface DictionaryMapper extends GenericDao<DictionaryDataEntity, Integer> {

    /**
     * 根据类别ID查询数据字典
     * @param classId 类别ID
     * @return 数据字典列表
     */
    List<DictionaryDataEntity> selectByClassId(Integer classId);


    /**
     * 根据查询条件查询数据字典
     * @param criteria 查询条件
     * @return 数据字典列表
     */
    List<DictionaryDataEntity> selectAll(DictionaryCriteria criteria);
}


@Repository
public class DictionaryDaoExtension implements DictionaryMapper {
    @Resource
    private RedisCacheUtils redisCacheUtils;
    @Resource
    private DictionaryMapper dictionaryMapper;

    @Override
    public List<DictionaryDataEntity> selectByClassId(Integer classId) {
        if (classId == null) {
            return new ArrayList<>();
        }
        RedisCacheUtils.RedisKey redisKey = this.buildRedisKey();
        String cached = this.redisCacheUtils.getHash(redisKey, classId);
        if (!StringUtils.isEmpty(cached)) {
            return JsonUtils.fromJsonToList(cached, DictionaryDataEntity.class);
        }
        List<DictionaryDataEntity> result = this.dictionaryMapper.selectByClassId(classId);
        if (result != null && !result.isEmpty()) {
            RedisCacheUtils.RedisKeyLifeCycle lifeCycle =
                    new RedisCacheUtils.RedisKeyLifeCycle(CACHE_TIME_OUT, TimeUnit.DAYS);
            this.redisCacheUtils.setHashValue(redisKey, classId.toString(), JsonUtils.toJson(result), lifeCycle);
        }
        return result;
    }

    @Override
    public DictionaryDataEntity getById(Integer id) throws DataAccessException {
        return this.dictionaryMapper.getById(id);
    }

    @Override
    public int deleteById(Integer id) throws DataAccessException {
        throw new UnsupportedOperationException();
    }

    ……
}

  这里的“DictionaryDaoExtension”使用了一个装饰模式,继承于“DictionaryMapper”并包含了一个“DictionaryMapper”类型的实例,“selectByClassId”方法中加上了缓存相关的操作。

三、领域服务

  领域服务是DDD战术阶段中定义的一个重要模型,当某个方法无法区分其属于哪个领域实体的时候一般会放到领域服务中。此外,如果一个方法涉及多个模型也就是所谓的跨模型操作,一般也会放到领域服务中。这里面需要注意的是领域服务和领域模型是一样的,最好只依赖于JDK。我见过一些设计,作者将“资源库(Repository)”注入到领域服务或领域模型中,个人比较不推荐这种方式,与Spring框架耦合过于严重了。

  在继续讲之前还需要说一下所谓的面向对象编程(OOP)到底是什么东西。作为对比,我们先说一下面向过程,简单来说就是根据业务流程说明一行一行的写代码最终完成整个用例,比较直观和简单。OOP如果用白话去说就是:在一个业务场景(用例)中,把涉及到的对象全拿出来,每个对象执行属于自己责任的任务。一般来说,需要通过应用服务来控制各对象的行为。之所以在领域服务中介绍这一段内容,是因为有一种设计模式:为每一个用例都建立对应的领域服务,应用服务不再直接调用领域模型而是转面调用此领域服务,再由后者调用领域模型。现实情况中大部分用例在每个子事务(一个用例可能涉及多个事务,使用最终一致性)中只会有一个领域模型参与,所以具体如何使用看个人习惯以及是否有必要。

  前后端分离和微服务架构已成为当前主流的设计方式,RESTfu、Web API或其它RPC是前后两端以及微服务间交互的主要手段。所以在我们的开发过程中往往会设计一些适配器组件比如“rest”层用于将当前系统的能力以RESTful接口的方式提供出去。虽然“rest”层的代码比较符合服务的定义,但一般并不会将其视为服务来看,其属于适配器(可参看六边型架构。严格上来讲DAO的实现也是一种适配器,不过鉴于其在开发过程中的戏份较多,我将其看作一种服务来对待)的一种,除了用于实现REST能力几乎不包含任何业务或数据逻辑,更多的是透传操作,类似的还包括各类工具类等。

  三个服务讲完了,虽然软件设计中可能还包含各种其它的“类服务”组件,但由于分量不足达不到主角的层次,撑死了是个二线配角,没流量。现在您仔细品味一下,会发现在开发过程中您所主要涉及的除了模型就是服务,没其它的了。那是不是说开发其实就已经有了一个整体的思路或模式了呢?比如“先设计模型,后设计服务”这种?这属于我个人的总结,您也许会有自己专用的模式。想要开发效率咱不能无脑的干,也得想想是否有现成儿的最佳实践或总结出适合于自己的方式,这叫态度。

四、归纳

  结合前面战术部分所讲的内容,我们总结出来在软件开发与设计流程中会涉及9种对象,这9种对象几乎涵盖了系统中的方方面面。各对象的概念前面已经做了详细介绍,下面的列表仅展示定义。

  • 软件开发流程三类模型:业务模型、分析模型、设计模型
  • 软件设计中的三个模型:数据模型、视图模型、领域模型
  • 软件设计中的三个服务:数据服务、业务服务、应用服务

  虽然重复,在这里仍然把前面所用的图总结性的贴出来供参考。

戏说领域驱动设计(十二)——服务-LMLPHP

总结

  本章主要讲了3个服务的概念和软件开发流程中所涉及的9类对象。后面的内容中还会对部分内容做细讲。其实很多的概念您仔细看都特别简单,而我的只是把这些内容进行了总结归纳。最重要的是让我们的开发有据可循、有理可循,不能盲目的进行。

03-09 13:31