DDD 独立类模式你用到了吗
一、背景
在前一段时间里有一篇文章比较火,说 service 必须要有个实现接口吗?这个在各大公众号上进行转载,引起 Java 技术网友的广泛评论,我也简单说了下个人的观点。最近正好也在不同的场景用到了独立类,没有实现接口。本文将结合 DDD 的独立类模式来重点探讨如何用好独立类,如果才不一刀切的看待独立类的使用。
二、独立类简述
2.1 独立类的简单概括
简单来说就是没有实现接口,没有实现抽象类的 Java 类,当然枚举和注解这里不是我们讨论的话题。
2.2 哪些算是独立类
通常来说很少有机会对模型进行实现接口比如 DTO 继承接口这样的操作我是几乎没见过。那这里说的独立类一般指的可能就是 Utils 工具类,比如 SpringConextUtils 或者 XXHelper 这种偏工具型单个类,一般这些类都提供了一些静态方法方便调用。另外一些就是比较常见的比如 controller 类,通常对于 controller 类来说如果没有太多需要 controller 是可以作为独立类的,但是有两种情况会比较常见,比如通用的操作会放到 BaseController 里面,第二种情况就是将 controller 里的 API 方法作为 feign 接口提供 RPC 调用。下面我们来讨论一下在 service 服务类层面的独立类场景,一般来说我们从命名上可以分为 xxFactory,xxServiceHelper,xxService,xxHandler,xxExecutor,xxBuilder,xxRepository 等等。这些服务类有的作为业务主流程代码的核心逻辑,有的则是为了方便数据处理或者辅助业务流程在某个节点可以按预期走下去。从这里就可以看出我们讨论的内容就是这些东西了。
2.3 DDD 的独立类模式
这里我们看一下 Eric 书里的重点内容:低耦合是对象设计的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变得完全独立了,这就使得我们可以单独地研究和理解它。每个这样的独立类都极大地减轻了因理解MODULE 而带来的负担。从上面的概念内容结合文中独立类章节可以看出这里的独立类可能说的比较广泛,eric 将独立类当作一种解决对象之间复杂依赖的一种方法。比如一个聚合根,那这里这个聚合根对象将不再引用其他对象,只引用自己聚合范围内的对象。所以对于 servic 服务逻辑的处理也是一个道理,就是为了降低对象引用的复杂度,从某个角度切断一些对象的联系。保持一定的对象引用关系,这里 eric 书里也重点讲了这句话:低耦合是减少概念过载的最基本办法。独立的类是低耦合的极致。因为对象出现复杂引用会导致我们用更多的概念去解释增加理解复杂度,比如在群里讨论的 RoleUserRelationBO 对象。对这个对象的定位大家都有不一致的看法,那还有一个引申出来的模式就是闭合操作模式,就是将一些对象(实体或者只对象)的内在操作归属于一个模型类,这个模型类你可以认为是一个独立类,至于是 service 或者或者 xxEntity 或者 xxValueObject 都不再重要了,因为这个类把这个概念的本身和其能表达的行为都封装在一个类里了,对于外界来说引用他就等于引用了这个整体。
三、应用境界
我在应用独立类之前也很随意,比如大多数情况下我也会先定义接口,在这些项目中我也经历了下面的几个阶段,大家可以参考下看看。
3.1 不知道我用了独立类
有时候会突发其想想根据某个场景构建一个工具类专门解决某个场景下的复杂问题,那就新建 XXUtils。当然另外一种场景就是我喜欢把一些相对复杂的数据处理逻辑单独从实现了接口的 service 中拿出来作为 xxHelper。这里我可能只是把这个类当作一个简单的 Java 辅助服务类去看待了。
3.2 知道我用了独立类
在后面的一些阶段,我也尝试尽量去掉接口声明来直接构建一个 service 服务,里面当然也会变得相对复杂比如引用了其他 service,mapper 等。但是这个 service 可能就是某个核心逻辑的一部分,而不是作为主流程的分支的一部分。
3.3 知道我用了 DDD 的独立类
最近在构建 DDD 的实战项目代码时突然发现可以在 BO 和 VO 业务对象类里面构建这个对象的行为了,这个非常令人兴奋,因为这像是实际演练了 DDD 的独立类模式。在我开发的另外的业务场景组件里面我尝试先用独立类来构建不同场景下的业务行为的模型,作为 service 服务类。我总共构建了四个场景服务类,这些场景服务类都有一些相似的行为但是返回的模型都不一样。当然,因为这些服务类是操作了一组业务对象,所以有了一些相同的底层操作比如持久化和增删改等,那我又构建了四个独立服务类,这四个服务类分别对应了模型的数据来源于数据库,本地,配置中心和缓存。由于上面四个场景服务类需要依赖这四个与持久化和数据源获取相关的独立服务类,所以我不得不重新构建了一个接口让这四个服务类实现这一组接口方法,来屏蔽因为数据源不同导致的底层差异化。当然如果你想知道代码在哪里,那就告诉你是 component-kv 的组件代码。
3.4 知道我用好了 DDD 的独立类
当然很多时候用某个东西的时候只是当时觉得用好了,后面因为有变化才觉得用的不好,那这里可能就是比较高的境界了,如果用的好的话可以更好的应对当前的问题。在 eric 的书里是相对比较强调重构的,因为只有不断重构去调整才能得到一个相对问的服务工程或者服务模型。所以在不断重构中才可以知道这个类或者这个方法是不是真正的能解决新的问题。可能脑海里会存在一个意识就是我现在虽然用好了,不依然要面临重构吗?其实大可不必,重构无法避免,但是如果在一个比较好的代码基础上辅以更多的重构技巧的话修改代码是不可怕的。那这里就可以时刻提醒着开发者在建模或者构建类行为的时候需要考虑更多的领域概念和行为以及这个对象能否承担的起这个职责。
3.5 思维导图
跟着其他最近发的文章一样,本篇也总结了一个思维导图,觉得不错的话可以收藏下:
四、应用方法
在上面的应用境界里你也许已经得到了一些应用方法,这里再整理下相关问题,方便总结。
4.1 service 应不应该有接口这一层
回到本文最初的话题,这个 service 是否应该有接口这一层,当然单从 service 没有个性化多场景的需求时写接口确实不太需要。这里我要表达的一个核心思想是这个 service 的一些操作是否具有通用性,是否是面向这个领域服务业务操作,如果面向领域服务的话那这些方法会被哪些其他服务调用产生什么样的依赖是需要细细考量的。
也就是说当 service 承担起复杂业务的时候有些通用的行为能力肯定会出现,这些通用的行为能力如果不抽象方法的话复杂度会上升很多。如果从 curd 的角度来说的话那倒没啥,毕竟都可以放 service 里面做,至于好不好改能不能方便的维护就另说了。
4.2 独立类是不是可以大量应用
因为上面的话题可能大家都会觉得连 servic 都可以不用实现接口了,独立类是不是就代表着我可以在项目里大量应用了,那肯定不是的。因为独立类也有自己的局限性,用到恰到好处才可以发挥最大价值。
4.3 独立类的职责是什么
从上面的描述可以看出日常使用独立类模式跟 DDD 的独立类模式其实出发点还不太一样,那么这里就简要说明一下独立类的职责有哪些内容:
作为业务主流程 service 的辅助
作为业务服务的 service 核心操作封装函数
作为工具类,提供静态方法
作为衔接两个模块之间的上下文调用服务类
作为封装对象和其行为来标示整体业务实体概念(领域的角度)
五、总结
5.1 独立类的优点
不至于每个业务操作都优先从接口的角度定义
独立类一般职责会更加专注本身负责的内容
降低整体业务代码的方法复杂度,比如代码行数等
更明确的表达业务对象和概念以及业务行为,提供整体内聚的能力,表达对象的封装特性
在模块之间提供一定的空间进行解耦
5.2 独立类的缺点
容易滥用,不容易掌握好什么时候该创建独立类来解决对应的问题
独立类的职责可能容易模糊,有时候作为主流程的代码有时候作为分支流程的代码,这些代码其实并不容易维护
独立类被引用的时候容易产生业务对象和业务服务出现更复杂的引用情况,比如独立类内部引用了其他服务,其他的底层服务,或者被其他服务类引用。这样会造成一个更加复杂的服务类引用链路图,进而无法抵御需求变化带来的维护成本上升。
独立类作为 service 的时候内部方法更可能频繁被更改不符合开闭原则。
独立类作为 service 的时候有时候是不依赖其他服务类的,比如只做数据转换数据处理等等,但是有时候会依赖其他服务类,这样会导致 3 的情况发生。
独立类服务由于是相对定制的,所以出现更多场景的变化时不方便重构,抽出共性逻辑和能力
版权声明: 本文为 InfoQ 作者【神帅】的原创文章。
原文链接:【http://xie.infoq.cn/article/f1062eedadd64184a5395661c】。文章转载请联系作者。
评论