系统开发流程中的三模型、软件设计中的三模型和三个服务
软件设计过程中主要包含三类服务:领域服务、应用服务和数据服务,具体如下图所示。
一、应用服务
如果您学习过软件工程这本书,会发现在讨论设计相关的知识的时总是提一个词“控制类”。遥想当年,我在上这门课的时候对好多的词都不懂,比如“数据字典”、“控制类”、“用例”等,听着就不明觉厉,但实际上对这些东西完全没有什么概念,不过考试及格罢了。工作几年后,做了一堆系统,还是不太明白所谓的“控制类”到底是何方神圣。这个事儿说起来挺玄幻的,再后来就突然“顿悟”了,也可能是看资料看得多了突然有所感应。接触过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种对象几乎涵盖了系统中的方方面面。各对象的概念前面已经做了详细介绍,下面的列表仅展示定义。
- 软件开发流程三类模型:业务模型、分析模型、设计模型
- 软件设计中的三个模型:数据模型、视图模型、领域模型
- 软件设计中的三个服务:数据服务、业务服务、应用服务
虽然重复,在这里仍然把前面所用的图总结性的贴出来供参考。