Domain Primitive 使用推荐

简介: 最近对团队的很多同学代码进行了 Code Review ,发现存在很多问题。其中一个问题就是普遍代码内聚不够,将原本需要对象提供的方法外泄给使用者。我们写惯了 贫血模型 代码的缘故,即只为对象定义属性、赋值和取值方法,将业务逻辑统一放到 Service 层来处理。更多地是面向步骤编程,而不是面向业务编程。

一、前言

最近对团队的很多同学代码进行了 Code Review ,发现存在很多问题。
其中一个问题就是普遍代码内聚不够,将原本需要对象提供的方法外泄给使用者。
在这里插入图片描述

如一个对象里包括状态字段,使用方需要根据状态判断是否为成功:

public class SomeResult{

    // 值 为  0 表示成功
    private String status;
    
    // 标签,其中 HOT 表示热门
    private String tage;

    private Map<String, Object> extInfo;
   // 省略其他

}

使用方:

    if("0".equals(result.getStatus()) && "HOT".equals(result.getTag())){
          // 执行某段逻辑
    
    }

Map<String, Object> extInfo = result.getExtraInfo();
extInfo.put("xxx", YYY);

本质上使用方只需要让 result 对象 “告诉” 自己是否成功、是否是热门素材即可。

但由于 Result 对象只有属性和 Getter 和 Setter 方法,没有其他属性,这部分逻辑就需要外部去感知。


本质上是因为我们写惯了 贫血模型 代码的缘故,即只为对象定义属性、赋值和取值方法,将业务逻辑统一放到 Service 层来处理。

更多地是面向步骤编程,而不是面向业务编程。

<br/>

二、存在的问题

<br/>

2.1 和软件设计原则违背

这违背 软件工程领域 “高内聚、弱耦合” 的设计原则。

在这里插入图片描述

同样也违反设计模式中的: 迪米特法则。

迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。英文简写为: LOD。

更违背 “封装复杂度” 的类设计原则。

<br/>

2.2 不必要的魔法值

类似的场景经常需要外部去感知具体的状态码、错误码,需要通过各种状态码去判断,然后再执行对应的逻辑。

即使接口提供方给出了枚举,上游也感知到不必要的逻辑。
接口提供方如果没有给出枚举,使用方还需要自己去定义常量或者枚举。

然而,大多数同学是喜欢偷懒的,通常直接用魔法值去判断,造成可读性极差。

阅读代码人压根不知道各种数字或者字符串代表什么含义。

<br/>

三、解决之道

3.1 充血模型

对象不仅包含数据,还包含属于它自己的操作。

想了解更全面的内容需要系统学习 DDD 相关知识。

推荐一些读物:

但是现在很多团队的很多项目并没有采用领域驱动设计的思想和架构进行开发。

但,这并不妨碍我们采用类似领域驱动设计中的一些理念去设计类。

我们可以了解下 Domain Primitive ,将完全贫血的类设计为 Domain Primitive 甚至设计为一个 DDD 中的实体和聚合根等。

3.2 Domain Primitive

3.2 部分转载自《阿里技术专家详解 DDD 系列- Domain Primitive》

3.2.1 Domain Primitive 定义

Domain Primitive (简称 DP)是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object 。

  • DP 是一个传统意义上的 Value Object,拥有 Immutable 的特性
  • DP 是一个完整的概念整体,拥有精准定义
  • DP 使用业务域中的原生语言
  • DP 可以是业务域的最小组成部分、也可以构建复杂组合
注:Domain Primitive的概念和命名来自于Dan Bergh Johnsson & Daniel Deogun的书 《Secure by Design》。

3.2.2 使用 Domain Primitive 的三个原则

  • 让隐性的概念显性化
  • 让隐性的上下文显性化
  • 封装多对象行为

3.2.3 Domain Primitive 和 DDD 里 Value Object 的区别

在 DDD 中, Value Object 这个概念其实已经存在:

在 Evans 的 DDD 蓝皮书中,Value Object 更多的是一个非 Entity 的值对象

在 Vernon 的IDDD红皮书中,作者更多的关注了Value Object 的 Immutability、Equals方法、Factory方法等

Domain Primitive 是 Value Object 的进阶版,在原始 VO 的基础上要求每个 DP 拥有概念的整体,而不仅仅是值对象。在 VO 的 Immutable 基础上增加了 Validity 和行为。当然同样的要求无副作用(side-effect free)。

3.2.4 Domain Primitive 和 Data Transfer Object (DTO) 的区别

在这里插入图片描述

3.2.5 Domain Primitive 的使用场景

常见的 DP 的使用场景包括:

  • 有格式限制的 String:比如Name,PhoneNumber,OrderNumber,ZipCode,Address等
  • 有限制的Integer:比如OrderId(>0),Percentage(0-100%),Quantity(>=0)等
  • 可枚举的 int :比如 Status(一般不用Enum因为反序列化问题)
  • Double 或 BigDecimal:一般用到的 Double 或 BigDecimal 都是有业务含义的,比如 Temperature、Money、Amount、ExchangeRate、Rating 等
  • 复杂的数据结构:比如 Map<String, List> 等,尽量能把 Map 的所有操作包装掉,仅暴露必要行为。

<br/>

3.2.6 示例

public class PhoneNumber {
  
    private final String number;
    public String getNumber() {
        return number;
    }

    public PhoneNumber(String number) {
        if (number == null) {
            throw new ValidationException("number不能为空");
        } else if (isValid(number)) {
            throw new ValidationException("number格式错误");
        }
        this.number = number;
    }

    public String getAreaCode() {
        for (int i = 0; i < number.length(); i++) {
            String prefix = number.substring(0, i);
            if (isAreaCode(prefix)) {
                return prefix;
            }
        }
        return null;
    }

    private static boolean isAreaCode(String prefix) {
        String[] areas = new String[]{"0571", "021", "010"};
        return Arrays.asList(areas).contains(prefix);
    }

    public static boolean isValid(String number) {
        String pattern = "^0?[1-9]{2,3}-?\\d{8}$";
        return number.matches(pattern);
    }

}

<br/>

3.3 简化案例

开头的案例就可以进行优化,将复杂度封装在对象内部,方便上游使用。

public class SomeResult{

    // 值 为  0 表示成功
    private String status;

    private TagEnum tag;
   // 省略其他
   
   
    public boolean isSuccess(){
       return StatusEnum.SUCCESS.getCode().equals(status);
    }


    public boolean isHot(){
       return TagEnum.HOT == tag && isSuccess();
    }


    public void setXXX(XXX xxx){
        extInfo.put(XXKeyConstant.XXX, xxx);
    }

    public void getXXX(){
      return  extInfo.get(XXKeyConstant.XXX);
    }

当然,这个示例非常简单,只是希望帮助大家理解这种理念,实际开发中遇到的场景可能会复杂的多,需要封装的方法也可能会很多。

实践中大家可以将:

  • 参数的合法性校验
  • 业务状态判断
  • 该类属性相关的部分处理方法
  • 需要获取该对象的内部属性再进行的操作
  • ...

都封装到该对象中,降低耦合,封装复杂度。

<br/>

四、总结

大多数程序员,写惯了贫血模型代码,不愿意去学习领域驱动设计的理念和实践。

很多程序员,潜意识认为团队项目没有明确使用领域驱动设计的思想和框架,就不应该或者不需要编写充血模型代码。

这个世界并非非黑即白的,即使团队中并没有明确使用领域驱动设计,甚至贫血模型满天飞,我们一样可以将设计原则渗透到编码中。
<br/>

参考文章

《阿里技术专家详解 DDD 系列- Domain Primitive》

相关文章
|
人工智能 自然语言处理 搜索推荐
6个好用的AI写作工具合集,各种AI写作软件类型超全整理!
AI生成营销文案、生成文章标题、改写润色内容、写作素材搜集...好用的AI写作工具有哪些?
6个好用的AI写作工具合集,各种AI写作软件类型超全整理!
【最佳实践】如何用宜搭做商品进销存
宜搭支持通过直接配置实现进销存场景。支持的常用进销存场景有: 图书管理系统、会议室预定系统、积分管理系统等。现在,就以商品进销存为例,示意操作过程。
【最佳实践】如何用宜搭做商品进销存
|
安全 网络安全 数据安全/隐私保护
15000个Fortinet防火墙的配置文件被泄露,你的防火墙也在其中吗?
15000个Fortinet防火墙的配置文件被泄露,你的防火墙也在其中吗?
|
存储 关系型数据库 MySQL
MySQL主从复制原理和使用
本文介绍了MySQL主从复制的基本概念、原理及其实现方法,详细讲解了一主两从的架构设计,以及三种常见的复制模式(全同步、异步、半同步)的特点与适用场景。此外,文章还提供了Spring Boot环境下配置主从复制的具体代码示例,包括数据源配置、上下文切换、路由实现及切面编程等内容,帮助读者理解如何在实际项目中实现数据库的读写分离。
1662 1
MySQL主从复制原理和使用
|
Java 数据库连接 Maven
【YashanDB知识库】私有maven使用崖山JDBC驱动
本文介绍如何将YashanDB的JDBC驱动包(yashandb-jdbc-1.5.1.jar)安装到Maven本地仓库。通过使用`mvn install:install-file`命令,指定参数如`-Dfile`(jar路径)、`-DgroupId`、`-DartifactId`和`-Dversion`等,可完成打包。之后,在项目中只需在`pom.xml`中添加对应依赖即可使用该驱动,方便集成与管理。
|
存储 编译器 C语言
|
存储 关系型数据库 MySQL
【TiDB原理与实战详解】5、BR 物理备份恢复与Binlog 数据同步~学不会? 不存在的!
BR(Backup & Restore)是 TiDB 分布式备份恢复的命令行工具,适用于大数据量场景,支持常规备份恢复及大规模数据迁移。BR 通过向各 TiKV 节点下发命令执行备份或恢复操作,生成 SST 文件存储数据信息与 `backupmeta` 文件存储元信息。推荐部署配置包括在 PD 节点部署 BR 工具,使用万兆网卡等。本文介绍 BR 的工作原理、部署配置、使用限制及多种备份恢复方式,如全量备份、单库/单表备份、过滤备份及增量备份等。
|
Java API 项目管理
Java一分钟之-Gradle插件开发:自定义构建逻辑
【6月更文挑战第5天】Gradle插件开发详解:从入门到发布。文章介绍如何创建自定义插件,强调依赖管理、任务命名和配置阶段的理解。示例代码展示插件实现及避免常见问题的方法。最后,讨论插件的发布与共享,助你提升构建效率并贡献于开发者社区。动手实践,打造强大Gradle插件!
420 3
|
应用服务中间件 Linux nginx
FFmpeg开发笔记(四十)Nginx集成rtmp模块实现RTMP推拉流
《FFmpeg开发实战》书中介绍了如何使用FFmpeg向网络推流,简单流媒体服务器MediaMTX不适用于复杂业务。nginx-rtmp是Nginx的RTMP模块,提供基本流媒体服务。要在Linux上集成rtmp,需从官方下载nginx和nginx-rtmp-module源码,解压后在nginx目录配置并添加rtmp模块,编译安装。配置nginx.conf启用RTMP服务,监听1935端口。使用ffmpeg推流测试,如能通过VLC播放,表明nginx-rtmp运行正常。更多详情见书本。
757 0
FFmpeg开发笔记(四十)Nginx集成rtmp模块实现RTMP推拉流