您的位置:首页 > 业界新闻

复合充血模式和领域服务调度-阿里互联网法院项目

2018-03-03 20:12 447 查看
     在阿里也待啦一段时间,阿里成立啦新的子公司-共道网络科技,公司寄语:公道人,让天下没有难打的官司;
最近在做互联网法院项目;作为一个技术人员我觉得对于公司的技术有必要阐述一下 ,不断的学习和提高自己;今天着重说明一下项目引进复合充血模式
说到这里,终于到了讨论的正题——贫血、失血和充血模型。什么是贫血失血充血模型呢?简单来说

失血模型:模型仅仅包含数据的定义和getter/setter方法,业务逻辑和应用逻辑都放到服务层中。
贫血模型:贫血模型中包含了一些业务逻辑,但不包含依赖持久层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层中。可以看出,贫血模型中的领域对象是不依赖于持久层的。
充血模型:充血模型中包含了所有的业务逻辑,包括依赖于持久层的业务逻辑。所以,使用充血模型的领域层是依赖于持久层,简单表示就是 UI层->服务层->领域层<->持久层
胀血模型:胀血模型就是把和业务逻辑不想关的其他应用逻辑(如授权、事务等)都放到领域模型中。我感觉胀血模型反而是另外一种的失血模型,因为服务层消失了,领域层干了服务层的事,到头来还是什么都没变。
 可以看出来,失血模型和胀血模型都是不可取的,现在的问题是,贫血模型和充血模型哪个更加好一些。很久很久以前,人们针对这个问题进行了旷日持久的争论,最后仍然没有什么结果。这里有一些帖子可供回味:
贫血,充血模型的解释以及一些经验
总结一下最近关于domain object以及相关的讨论
双方争论的焦点主要在我上面加粗的两句话上,就是领域模型是否要依赖持久层,因为依赖持久层就意味着单元测试的展开要更加困难(无法脱离框架进行测试,原文的讨论中这里专指Hibernate),领域层就更难独立,将来也更难从应用程序中剥离出来,当然好处是业务逻辑不必混放在不同的层中,使得单一职责性体现的更好。而支持者(充血模型)认为,只要将持久层抽象出来,即可减少测试的困难性,同时适用充血模型毕竟带来了不少开发上的便利性,除了依赖持久层这一点,拥有更多好处的充血模型仍然值得选择。最后,谁也没能说服谁,关于贫血模型和充血模型的选择,更多的要靠具体的业务场景来决定,并不能说哪一种更比哪一种好。设计模式这种东西不是向来都没有什么定论么。
我个人则倾向使用充血模型,因为充血模型更加像一个设计完善的系统架构,好在计算机世界里有很多的IOC和DI框架,唯一的缺陷依赖持久层可以通过各种变通的方法绕过,随着技术的进步,一些缺陷也会被慢慢解决。我的思路是这样的:先将持久层抽象为接口,然后通过服务层将持久层注入到领域模型中,这样领域模型仅仅会依赖于持久层的接口。而这个接口,可以利用现有框架的技术进行抽象

组合充血模式    

充血模型的引入

  


大部分系统都采用了类似设计,其层次结构清楚,各层之间单向依赖,开发不需要理解复杂的设计思想,上手难度低。这都是其明显的优势。
但是其劣势也很突出,由于实体抽象只有属性没有行为,所以业务逻辑都采用面向过程的方式实现,造成了很多冗余代码。 这种贫血模型随着时间的推移和业务积累,调用关系越来越无序,类似图中Bo调用Mapper层的箭头,网状调用变得难以维护。
为了解决代码复用率低的问题,我们最初设置了一个约束,即Bo只能调用自己的Mapper,如下图:


我们设想一个Do与一个Bo的这种一一对应关系是一个整体,那么他恰好是一个同时包含了属性与行为的组合对象,即我们通常所说的充血模型。
充血模型强制了一个实体所有的行为只在Bo中提供,这样开发人员在实现1个业务功能时,会优先找到Bo中是否存在某行为,没有找到时也只能在该Bo中添加行为,从而强制了代码复用,避免了同一个逻辑到处写的尴尬。
与传统的充血模型相比,两个一一对应的类组成一个充血模型,避免了将事务引入Do,同时又方便Mybatis的对象生成,这种Do、Bo组成充血模型的方式,我们称之为组合充血模型。

框架抽象

基于组合充血模型的理念,我们可以对Do、Bo进行框架层面的抽象,其结构如下:



其中上半部分Lava框架提供的基础类,下半部分则由框架自动生成,而小黑框中则是由开发人员自己编写的业务代码。

LavaBaseModel

LavaBaseModel是所有模型的基类,他主要提供2个功能:

1、ID加解密

为了数据安全,框架默认所有前后端交互过程中的Id都是加密的,即任何Do、Vo、Dto,在序列化为JSON字符串时,getId()方法不参与序列化,转而提供getSecurityId()方法。同理,反序列化时,接收Security字段后,解密出Id字段。后端传值时getId()方法不受影响。
SecurityIdUtil对于ID加解密,默认有3种实现,分别为使用固定的秘钥加解密、从UserContext上下文中获取秘钥加解密、不加密。如果应用未注入此3个Bean中的任何1个,则默认不加密。推荐使用用户上下文秘钥,既可针对用户设置秘钥,也支持所有用户配置同一个秘钥,比较灵活,注入配置为:
<!-- Id加解密 -->
<bean class="com.alibaba.lava.rich.security.UserContextSecurityIdHandler" />

2、Model自由转换

系统中存在与数据库打交道的Do、接口间传递参数的Dto、前端展示用的Vo等各种模型,这些模型通常需要互相转换,为了避免转换代码到处写,并且屏蔽内部实现细节,Lava框架提供了TransSupport接口,并规定所有Model类型都需继承LavaBaseModel类,利用JDK8的接口默认实现特性,即可完成Model间自由转换,示例如下:
UserDTO userDto = userDO.trans(UserDTO.class);
LavaDo提供了默认的创建者、修改者、是否删除等字段,并提供getBo()方法与Bo对应。所有系统生成的Do都继承该类。
LavaBo有一个默认实现AbstractLavaBoImpl,提供了单表增删改查的默认实现,其中所有***ByExample()的方法,都被设置为protected,因为我们认为,一旦使用了Example,代表包含了具体的业务逻辑,只应在本Bo或子类中使用,需要对外提供服务时,需要开发人员包装成具有明确业务含义的public方法。

LavaDo、LavaDto

LavaDo封装了业务系统的5个基础字段:创建时间、修改时间、创建人、修改人、是否删除。这些字段仅作为系统定位问题用,不能作为业务字段。比如常用的将创建人作为提交人显示在页面上,用创建时间排序等。
而删除也采用软删除的方式,将IS_DELETED字段置为y,使得数据可追溯。
LavaDo只用于Bo和数据层打交道,而接口间交互数据,返回数据到前端等场景,则需要用到LavaDto。两个模型之间可以使用trans接口互相转换,而如果有特殊的转换场景,也可以重写该方法自己添加逻辑。
Lava框架没有Vo的概念,但是如果业务中有需要使用的场景,也可以通过继承LavaBaseModel实现互相转换的效果。

一、领域的引入

我们现有架构,基本脱胎于传统MVC分层+充血模型。通过框架约束,Bo不再混乱的调用Mapper,达到了Bo中业务内聚的效果。这种内聚有效保证了业务代码的复用,同时使得Bo中增删改查抽象,并自动生成代码成为可能。这种将Bo和Mapper一一对应的方式,是将属性和行为一体的充血模型思想。在我们项目使用这种架构运行了近2年后,其好处是显而易见的。拿到1个功能,大部分时候开发同学能明确的知道要在哪个Bo中修改,也知道需要去哪个Bo中寻找需要调用的接口,重复代码大幅减少。但是随着时间推移,业务越来越庞杂,其不足之处也慢慢体现出来。其一,对于1个核心业务很集中的系统,他的某些Bo会非常臃肿。比如整个诉讼流程都是依赖案件,那么几乎所有其他Bo都会需要与案件Bo发生关系,这导致添加到案件Bo中的方法越来越多,接近于无法理解。其二,很多时候,业务需要联合几个Bo完成,这种关联的业务不依赖于单独的Bo,严格来说不属于单个实体的行为,对充血模型是1种污染。其三,业务发展规模增大以后,需要抽取其中一些业务作为单独的模块或者应用时,由于其中Bo调用盘根错节,迁移代价非常大。其四,1个开发想要熟练的完成1个业务功能时,他必须对全系统的Bo都非常熟悉。而不够熟悉的开发,则会在找不到需要的服务时,自己写上一套,于是充血模型的约束不再生效,代码也趋向于无序。这时候,我们希望通过架构的优化,来避免这些问题。对于臃肿的Bo,需要对其进行拆分,而拆分的维度该如何选择呢?对于Bo的业务组合和业务隔离,也需要对Bo组进行拆分,拆分维度又该如何选择呢?我们想到了领域驱动。

二、什么是领域驱动

领域驱动不是一种具体技巧,而是一种思想,是面向对象思想的进一步提炼和总结。对于领域拆分,业界没有一个统一的标准,但是这种思想可以通过阅读一些文章来感受下,如:领域驱动设计精简版.pdf(1.33 MB) 领域驱动依赖于对业务模型的精确还原。一个领域模型只关心一个有边界的问题域中的本质,他与软件具体实现无关,且贯穿整个项目过程。当需求、设计、开发都面向同一个领域模型进行沟通时,能利用通用语言建立高效的沟通,且有效防止需求走样。本篇不讨论领域服务的拆分维度和方法,而是需要构建出一个合适的模型,使得领域服务拆分以后,有效的兼顾服务的通用性和扩展性,并使得服务调用者更便捷。

三、领域服务模型

领域拆分后,在开发过程中,怎么约束开发人员不破坏领域边界,这是需要在框架层面约束的。毫无疑问,最好的保证领域的纯洁性的办法是服务向上依赖和领域调用隔离。因为开发总是习惯性的选择最小修改方案,而非最正确的方案。当拿到1个需求时,大多数开发会在1个Bo中从头写到尾,当调用其他Bo时,即使涉及到可重用逻辑,也倾向于在本Bo中实现。这种Bo之间的横向调用组成了一个网状结构,使得理解逻辑和维护变得困难。当领域被划分以后,我们约束1个领域的Bo不能使用另一个领域的Bo,而只能使用其提供的域服务。这种约束可以通过拆分微服务应用实现,域服务是抽象在Bo之上的一层,域服务之下是领域内自治,对领域之外完全透明。然而单纯的抽象一层Service,对于问题本质并没有特别大的改善,只不过是将写在Bo中的混乱逻辑,移到了Service层,时间一长,蛛网调用又会重现。所以,如何厘清Service和BO的关系,则变得尤为重要。DomainService是一个领域对外出口,理论上只存在唯一一个DomainService,他的方法由领域Owner抽象和设计,任何针对DomainService的修改都需要由owner确认。Owner需要考量领域方法的重用性、扩展性、可维护性、易用性、性能指标、可测试性,以及判断是否有替代方法。BO与DO一一对应组成一个实体的属性和行为,他只关注于该实体本身,不应过多涉及其他实体的业务,特别是其他领域的业务(当然也有例外,比如发个消息等操作)。而一个领域内联合多个实体的业务,则可以通过抽象一个Service来实现,如上图右侧。Service不同于DomainService,他不对外暴露服务,只用于内部业务组装。

四、领域服务路由

经过上面的抽象,我们将应用拆分成多个微服务,每个微服务提供了1个唯一的DomainService接口,从而将业务模块进行解耦,方便了应用维护。这同时引入了另一个问题,如果一个领域所有的业务都由一个域服务来做,他是不是会变得特别庞大。举个例子,一个证据服务,针对交易纠纷他有一类证据,需要从交易平台获取,针对合同纠纷又需要用户自己上传,针对支付令,则是对接催收系统获取。这些不同业务的接口统一暴露在1个域服务中,是不太合理的,为了抽象而抽象,反而使域服务过于臃肿。我们引入了扩展域服务的概念,他用于处理一个领域内,独立于通用域服务之外的特殊业务逻辑处理。以纠纷域为例,如下图,除了基础域服务的新增纠纷,修改实体等基本服务外,针对案件类的纠纷,还有计算诉讼费、撤诉、申请执行等特殊业务,这些不能抽象为基础服务的域服务,单独使用一个Service接口对外提供。这里需要注意的是,扩展接口不继承基础接口,但是扩展服务的实现类需要集成基础服务的实现类。这是为了调用方在使用服务时,默认都使用基础服务,由服务提供方来决定服务如何路由到实现类中,而调用方只有在明确扩展业务时才使用扩展服务。针对域服务的路由,框架设计了一个DomainServiceDispatch来负责调度具体服务,而开发者可以在应用中注入不同的路由器来实现不同的路有逻辑。Lava框架在lava-starter二方包中已经提供了根据案由和租户来路由的版本。如果未配置路由器,则直接找借口的默认实现类。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐