两种不同充血模型实现中的问题及解决办法-阿里云开发者社区

开发者社区> 1271405475347552> 正文

两种不同充血模型实现中的问题及解决办法

简介: ### 前言 关于贫血模型与充血模型,已经有大量的文章写到了,但大部分都只是写了两种模型的对比,其中的例子也相对比较简单。 当我们真正在使用充血模型的过程中,还会碰到很多问题,本文期望通过我们复杂的业务场景,深入的讲解我们在真正使用充血模型时,到底会碰到哪些问题,我们又要如何来解决。 ### 什么是充血模型 Martin Fowler在2003年发表的一篇文章中,第一次提出贫血模型的概念,他把贫
+关注继续查看

前言

关于贫血模型与充血模型,已经有大量的文章写到了,但大部分都只是写了两种模型的对比,其中的例子也相对比较简单。
当我们真正在使用充血模型的过程中,还会碰到很多问题,本文期望通过我们复杂的业务场景,深入的讲解我们在真正使用充血模型时,到底会碰到哪些问题,我们又要如何来解决。

什么是充血模型

Martin Fowler在2003年发表的一篇文章中,第一次提出贫血模型的概念,他把贫血模型称为反模式,因为贫血模型完全违反了面向对象的基本原则。贫血模型 by Martin Fowler
而充血模型是一种有行为的模型,模型中状态的改变只能通过模型上的行为来触发,同时所有的约束及业务逻辑都收敛在模型上。

贫血模型相对简单,模型上只有数据没有行为,业务逻辑由xxxService、xxxManger等类来承载,相对来说比较直接,针对简单的业务,贫血模型可以快速的完成交付,但后期的维护成本比较高,很容易变成我们所说的面条代码。
充血模型的实现相对比较复杂,但所有逻辑都由各自的类来负责,职责比较清晰,方便后期的迭代与维护。

在国内还有人做了更细的命名,在这种分类下,分成了4种模型:失血模型、贫血模型、充血模型和涨血模型。我这里采用Martin Fowler的命名方式,个人觉得其它几种只是充血模型的不同实现而已。

充血模型的两种实现方式

因为模型状态变更后,还需要做持久化,而持久化需要用到基础设施层的能力,因而充血模型的实现没有特别完美的方式,主流的实现有两种,但都有一定的问题:
实现一:不带Repository的充血模型实现:模型中不带有Repository,调用模型的方法后,内存中的对象状态变更,再通过调用Repository来做数据的持久化。
实现二:带Repository的充血模型实现:模型中带有Repository,调用模型的方法后,模型会自己完成数据的持久化。

另外在我们实现充血模型时,有一个重要的原则:

不管是充血模型的哪一种实现,有一个重要的原则是充血模型中不能有重要属性的set方法,
改变这些属性有且只有通过模型上的行为方法。 比如在Account上不能有setStatus方法,只能有enable方法。

因为我们在enable方法上是有业务逻辑约束的,而setStatus方法是没有任何约束的,我们不能无约束的改变Account的状态。

实现一:不带Repository的充血模型实现

不带Repository的充血模型使用方法如下:

@Transactional(rollbackFor = Exception.class)
public void accountEnable(Long accountId) {
    //查找帐号
    Account account = accountRepository.find(accountId);
    //激活帐号,改变内存中帐号的状态
    account.enable();
    //持久化
    accountRepository.save(account);
}

Account的enable方法实现如下:

/**
 * 启用账号
 */
public void enable() {
    // 前置检验
    if (this.status != AccountStatus.DISABLE) {
        throw new AccountBizException(AccountErrorCode.ACCOUNT_STATUS_ERROR, "账号非关闭状态,无法启用,账号id:" + id);
    }
    // 变更状态
    this.status = AccountStatus.ENABLE;
}

在上面的实现中我们有三个问题还需要解决:

问题一:如果Account上有各个属性的set方法,就能绕过enable或其它状态变更时的约束。

在使用这个Account对象时,如果我们直接调用account.setStatus(AccountStatus.ENABLE),然后再调用Repository.save方法,我们就能直接改变帐号的状态,这样就绕过了所有状态变更的约束了。
这个问题相对好解决,我们可以把Account上的set方法去掉,然后在account中内置一个builder,通过这个builder来构建account对象,或者通过工厂来构建account对象,比如:

        // 通过builder构建account对象
        Account account = Account.builder()
            .tenant(accountCreateCmd.getTenant())
            .bizAccountId(accountCreateCmd.getBizAccountId())
            .mobile(accountCreateCmd.getMobile())
            .tbAccount(accountCreateCmd.getTbAccount())
            .idCard(accountCreateCmd.getIdCard())
            .nick(accountCreateCmd.getNick())
            .registerTime(LocalDateTime.now())
            .build();
        //持久化account对象
        accountRepository.save(account);
问题二:在领域对象中发布领域事件时,对象还没有做持久化,会导致收到事件时在数据库中查不到数据。

在前面的例子中,我们看到当我们在调用account.disable方法时,只是改变了对象在内存中的状态,并没有做持久化,而此时在disable方法中,我们会发布一个accounDisable的领域事件,发布事件时还没有做持久化,接收到这个事件时去查询account的状态就会出问题。
这个问题的解法也不难,我们需要把领域事件的发布拆分成两个阶段:
第一个阶段是创建事件,在account.disable方法中。
第二个阶段是发布事件,并清除所有事件,在repository.save方法中。
在disable方法中代码如下:

    /**
     * 禁用账号
     */
    public void disable() {
        // 前置校验
        if (this.status != AccountStatus.ENABLE) {
            throw new AccountBizException(AccountErrorCode.ACCOUNT_STATUS_ERROR, "账号非开启状态,无法启用,账号id:" + id);
        }
        // 变更状态
        this.status = AccountStatus.DISABLE;

        // 创建领域事件
        this.createEvent(new AccountDisableEvent(this.id));
    }

领域模型基类,我们把事件的创建与清理统一放在基类中,所有领域对象都需要继承它。

public abstract class AggregateRoot<T> {

    private final List<T> domainEvents = new ArrayList<>();

    /**
     * All domain events currently captured by the aggregate.
     */
    public List<T> domainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }

    /**
     * Registers the given event for publication.
     */
    protected void createEvent(T event) {
        this.domainEvents.add(event);
    }

    /**
     * Clears all domain events currently held. Invoked by the infrastructure in place in Spring Data
     * repositories.
     */
    public void clearDomainEvents() {
        this.domainEvents.clear();
    }
}

在repository.save方法中,我们会发布所有创建的事件,并清除创建的事件(以免对象再次变更时重复发送事件),代码如下:

    public void save(Account account) {
         //1. account对象持久化代码
         //省略持久化代码.......

        // 2. 发布领域事件
        account.domainEvents().forEach(event -> {
            applicationContext.publishEvent(event);
            log.info("发布领域事件:{}", event);
        });
        //3. 清除已经发布的事件
        account.clearDomainEvents();
    }
问题三:有时只改了一个属性,save方法却要把整个对象全部保存,对性能有比较大的影响

在复杂业务中,一个对象都是特别的大,变更一个属性就要保存整个对象,对性能影响的确很大,我是否能在调用save方法时只保存更新过的字段呢?
要解决这个问题我们要用到一种比较复杂的技术:Change-Tracking(变更追踪),变更跟踪有两种实现方法:
1、基于Snapshot的方案:当数据从DB里取出来后,在内存中保存一份snapshot,然后在数据写入时和snapshot比较。
2、基于Proxy的方案:当数据从DB里取出来后,通过weaving的方式将所有能改变状态的方法都增加一个切面来判断是否被调用以及值是否变更,如果变更则标记为Dirty。在保存时根据Dirty判断是否需要更新。常见的实现如Entity Framework。

这种方法我并没有使用过,所以就不贴代码了,有使用过的同学可以给我提供一下。

实现二:带Repository的充血模型实现

带Repository的充血模型的使用方法如下:

@Transactional(rollbackFor = Exception.class)
public void accountEnable(Long accountId) {
    //查找帐号
    Account account = accountRepository.find(accountId);
    //激活帐号,注意因为Account带了repository,调用enable方法时已经完成了持久化
    account.enable();
}

Account的enable方法实现如下:

/**
 * 启用账号
 */
public void enable() {
    // 前置检验
    if (this.status != AccountStatus.DISABLE) {
        throw new AccountBizException(AccountErrorCode.ACCOUNT_STATUS_ERROR, "账号非关闭状态,无法启用,账号id:" + id);
    }
    //调用repository做状态持久化
    this.accountRepository.updateStatus(this.getId(), this.status);
    // 更新数据库成功后,变更内存中对象的状态,保持内存中对象为最新状态
    this.status = AccountStatus.ENABLE;
}

在上面的实现中,同样不完美,我们有如下问题需要解决:

问题一:repository中有更新状态的万能方法,调用这个方法将会绕过所有加在对象上的约束

当实现启用帐号方法时,我们通过repository中的updateStatus方法,做到了只更新一个字段,而不是调用save方法更新整个对象,这避免了上一个实现中最复杂的问题三,但repository中却要增加一个万能方法,对此没有好的办法,我们在实践过程中,是通过一个规约来约束大家的,规约如下:

所有对领域对象的写操作,必须通过领域模型,不能直接调用repository的方法
问题二:对象中持有repository,导致模型与repository耦合,不美观

这个问题也没有办法解,对于有代码洁癖的同学来说,只能放弃这种实现了。
但我们要回过头来看看,领域模型与repository的耦合是问题吗? 领域模型与repository都是在领域层的,同一层意味着相互之间是可以引用的,他们本来就是耦合在一起的。同时领域层的repository只定义了接口,而实现是在基础设施层,因此领域层的repository并没有关于持久化的实现,因此就算领域模型中持有repository的接口,也不会也持久化的相关技术耦合。
因此模型与repository的耦合其实是不违反面向对象的SOLID的设计原则,也不违反我们的分层原则,为什么大家会认为它不美观呢?如果你的Repository中有数据库实现的细节,那有可能是你的设计违反了DIP(依赖倒置)的分层原则。

总结

没有完美的方案,只有适合业务场景的方案,所有的架构都是做取舍,并不一定要在项目中使用充血模型才是最好的,也不一定要使用哪种充血模型的方案,明白每一个方案的优点和缺点,选择合适的方案才是架构真正要做的事。
我们在项目的实践过程中,这两种充血模型都有用过,不带Repository的充血模型用在我们的劳动力主数据管理中,带Repository的充血模型用在我们排班系统中。整体来说这两种方案,第一种实现难度会比第二种大一些,如果是第一次使用充血模型,建议用第二种方案。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
两种不同充血模型实现中的问题及解决办法
### 前言 关于贫血模型与充血模型,已经有大量的文章写到了,但大部分都只是写了两种模型的对比,其中的例子也相对比较简单。 当我们真正在使用充血模型的过程中,还会碰到很多问题,本文期望通过我们复杂的业务场景,深入的讲解我们在真正使用充血模型时,到底会碰到哪些问题,我们又要如何来解决。 ### 什么是充血模型 Martin Fowler在2003年发表的一篇文章中,第一次提出贫血模型的概念,他把贫
59 0
怎么设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程
7203 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4556 0
VS“当前不会命中断点。源代码与原始版本不同”的问题的有效解决办法
我的解决方法是: 删除项目目录下的Obj/Debug目录^_^   网络的解决方案:   开发时有一个工程的一个文件的断点无效,VS 2005提示说当前不会命中断点。源代码与原始版本不同,请在断点选项里设置允许源代码与原始版本不同。
1901 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
8030 0
中英字体不同导致的下划线不对齐问题
如果网页中定义的中英文字体不同,这会导致下划线不对齐。这种情况在IE浏览器中存在,Firefox浏览器无此问题。解决办法是: a:hover{     border:none;     text-decoration:underline;     color:#ff9900; ...
605 0
多模块Struts应用程序的几个问题(及部分解决方法)
Struts从1.1版本开始支持把应用程序分为多个模块,每个模块可以看作独立的应用程序,在带来方便的同时,我也发现了一些问题。比如有一个struts应用程序分了大约十个模块,现在有以下问题不知道大家一般是怎么解决的: 1、因为要进行验证,所以在每个模块对应的资源文件里都要有“errors.
827 0
服务器版本更新与客户端不同步的问题
http状态304表示请求的是缓存,200表示是从服务器请求的。 3张不同的照片,第一次访问,总共请求了4次, Insert title here       然后我们刷新一下,发现200的变成了304,因为图片已经缓存在了本地。
692 0
+关注
1
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载