DDD重构之路 - 0贫血模式的问题.md

293 阅读5分钟

写在前面

学习了殷浩大神的DDD系列文章后,尝试重构原先数据中台的系统架构,认为应该把这个实践过程记录下来。

在成长为架构师的道路中,降低系统复杂度确实是一个挑战。之前一直执着于用技术解决业务问题,例如设计模式、P of EAA、SOA等。而DDD(Eric Evans的Domain Driven Design)真正从业务角度出发,为做纯业务的开发提供了一套架构思路。不同于一套固定的框架,DDD是一种架构思想。在大佬的文章中提到了一篇博客:Anemic Domain Model,即贫血域模型在实际应用中出现问题的情况层出不穷,这在发展了2-3年的大型系统中经常出现,我在下文中也会举例说明。这让大部分人在实际应用中,把OO不可避免地转变为PO。

贫血模型带来的问题

现在的代码设计思路都习惯于开始先设计一堆DO、DTO、VO等等,然后将业务逻辑写在service层的接口里。这些DO、DTO、VO只包含了属性,不包含甚至不允许包含任何业务逻辑,这就是贫血模型。

以数据同步这个需求为例,假设最初的需求是要将线上业务数据同步至数仓,用于每日跑批,需要基于DataX实现MySQL同步至Hive。为了快速实现需求,可能设计思路如下:

首先设计基本数据结构:

public class MySqlDatasource {
    private String ip;
    private String port;
    private String username;
    private String password;
}
public class HiveDatasource {
    private String ip;
    private String port;
    private String username;
    private String password;
    private List<Object> otherConfigs;
}

编写获取DataX的执行配置文件:

public class DataSyncService {

    private String mysqlToHive(MySqlDatasource mySqlDatasource,
                               HiveDatasource hiveDatasource,
                               String fromDatabase,
                               String toDatabase,
                               String fromTable,
                               String toTable) {
        // 省略业务逻辑
        return config;
    }
}

很容易理解的一个业务逻辑,实现起来也很简单。后来业务稍微复杂了一点,要求Hive同步至Hive、Hive同步至MySQL、MySQL同步至MySQL,于是又新增了若干方法:mysqlToMySqlhiveToHivehiveToMySql。聪明的小伙伴觉得,稍微学点设计模式就不至于这么麻烦,于是代码变成了这样:

public class Datasource {
    private String ip;
    private String port;
    private String username;
    private String password;
}

public class MySqlDatasource extends Datasource {
}

public class HiveDatasource extends Datasource {
    private List<Object> otherConfigs;
}

public class DataSyncService {

    private String getConfig(Datasource fromDatasource,
                             Datasource toDatasource,
                             String fromDatabase,
                             String toDatabase,
                             String fromTable,
                             String toTable) {
        // 省略业务逻辑
        return config;
    }
}

类是变少了,getConfig方法里的逻辑可不简单,随着数据源增多,getConfig就要复杂很多,假设有n个数据源,if-else逻辑就有n * n个。

这就是贫血模型带来的问题:代码中存在很多只用于存储数据的bean,而业务逻辑在service层越来越臃肿,业务逻辑甚至会出现在controller里。随着业务复杂度提升,可阅读性可维护性都会后大大降低。以为是在写OO的代码,实际在写PO的代码。

那么针对上述这个需求我们是怎么优化的呢?了解DataX架构的应该知道,DataX是插件式架构,那么了解该架构的同学可能可以理解一下以下代码:

public interface DataSource {

    /**
     * 对应datax的reader插件名
     */
    String readerName();

    /**
     * 对应datax的writer插件名
     */
    String writerName();

    /**
     * 来源数据源
     */
    ReaderConfig parseReaderConfig(String confJson);

    /**
     * 目标数据源配置
     */
    WriterConfig parseWriterConfig(String confJson);
}

public class DataSyncService {

    private String getConfig(Datasource fromDatasource, Datasource toDatasource, FieldMapper fieldMapper) {
        // 非真实DataX配置文件逻辑
        JSONObject config = new JSONObject();
        config.put("readerName", fromDatasource.readerName());
        config.put("writerName", toDatasource.writerName());
        config.put("readerConfig", fromDatasource.parseReaderConfig().toString())
        config.put("writerConfig", toDatasource.parseWriterConfig().toString())
        config.put("fieldMapper", fieldMapper.toString())

        return config.toJSONString();
    }
}

每个数据源都继承该接口,用以上接口便能初步组装成一个可供DataX运行的配置文件。不同于原先的设计方式,这里把业务逻辑分散到datasource java bean中。相对完整的代码逻辑变为:从数据库获取datasourceDO -> 根据type转换为对应datasource -> 在service层根据接口组装DataX config,并提交任务

这其实就有一点DDD的影子了,service中展示了业务的流程,但是详细的业务代码在datasource java bean中。

结语

有人会说,就这?不就是活用设计模式吗?确实,想必各位老鸟为了代码的可扩展性、可阅读性、可维护性,或多或少都会代码进行一系列优化。肯定每个人都会有自己的写法。这个需求比较复杂就优化一下,那个需求暂时简单,service中写写业务就完事儿了。但是你能保证以后复杂度提升了不会又编程面向过程编码了吗?

一个好的代码架构能让系统安全、稳定、快速迭代。在一个团队内通过规定一个固定的架构设计,可以让团队内能力参差不齐的同学们都能有一个统一的开发规范,降低沟通成本,提升效率和代码质量。

在做架构设计时,一个好的架构应该需要实现以下几个目标:

  • 独立于框架:架构不应该依赖某个外部的库或框架,不应该被框架的结构所束缚。
  • 独立于UI:前台展示的样式可能会随时发生变化(今天可能是网页、明天可能变成console、后天是独立app),但是底层架构不应该随之而变化。
  • 独立于底层数据源:无论今天你用MySQL、Oracle还是MongoDB、CouchDB,甚至使用文件系统,软件架构不应该因为不同的底层数据储存方式而产生巨大改变。
  • 独立于外部依赖:无论外部依赖如何变更、升级,业务的核心逻辑不应该随之而大幅变化。
  • 可测试:无论外部依赖了什么数据库、硬件、UI或者服务,业务的逻辑应该都能够快速被验证正确性。

今天我们在做业务研发时,更多的会去关注一些宏观的架构,比如SOA架构、微服务架构,而忽略了应用内部的架构设计,很容易导致代码逻辑混乱,很难维护,容易产生bug而且很难发现。现在,今天,殷浩大神通过案例的分析和重构,提供了一套基于DDD的相对固定的代码架构,后续的文章会分享基于该架构重构老代码的过程。