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》

相关文章
|
SQL 缓存 Java
殷浩详解DDD系列 第三讲 - Repository模式
# 第三讲 - Repository模式 **写在前面** 这篇文章和上一篇隔了比较久,一方面是工作比较忙,另一方面是在讲Repository之前其实应该先讲Entity(实体)、Aggregate Root(聚合根)、Bounded Context(限界上下文)等概念。但在实际写的过程中,发现单纯讲Entity相关的东西会比较抽象,很难落地。所以本文被推倒重来,从Repository
37734 8
|
微服务 测试技术 Java
阿里技术专家详解 DDD 系列- Domain Primitive
关于DDD的一系列文章,希望能继续在总结前人的基础上发扬光大DDD的思想,但是通过一套我认为合理的代码结构、框架和约束,来降低DDD的实践门槛,提升代码质量、可测试性、安全性、健壮性。
61943 17
阿里技术专家详解 DDD 系列- Domain Primitive
|
缓存 前端开发 中间件
DDD 领域驱动设计落地实践系列:工程结构分层设计
前面几篇文章中,笔者给大家阐述了 DDD 领域驱动设计的三大过程,重点围绕如何通过战略设计与战术设计进行 DDD 落地实践进行了详细的讨论,但是还没有涉及到工程层面的落地。实际上所有的这些架构理论到最后都是为了使得我们代码结构更加清晰,从而开发出 bug 少、扩展性强、逻辑清楚的应用。因此本文就是为了解决 DDD 领域驱动落地实践最后一公里问题,将我们分析出来的领域模型通过与工程结构的映射实现真正的落地。
DDD 领域驱动设计落地实践系列:工程结构分层设计
|
消息中间件 网络协议 前端开发
殷浩详解DDD:如何避免写流水账代码?
在日常工作中我观察到,面对老系统重构和迁移场景,有大量代码属于流水账代码,通常能看到开发在对外的API接口里直接写业务逻辑代码,或者在一个服务里大量的堆接口,导致业务逻辑实际无法收敛,接口复用性比较差。所以本文主要想系统性的解释一下如何通过DDD的重构,将原有的流水账代码改造为逻辑清晰、职责分明的模块。
殷浩详解DDD:如何避免写流水账代码?
|
存储 自然语言处理 前端开发
领域驱动设计(DDD)-基础思想
一、序言     领域驱动设计是一种解决业务复杂性的设计思想,不是一种标准规则的解决方法。在领域驱动设计理念上,各路大侠的观点也是各有不同,能力有限、欢迎留言讨论。 二、领域驱动设计 DDD是什么 wiki释义:     领域驱动设计(英语:Domain-driven design,缩写 DDD)是一种通过将实现连接到持续进化的模型[1]来满足复杂
7823 0
|
canal 消息中间件 存储
DDD领域驱动设计实战(六)-理解领域事件(Domain Event)(中)
DDD领域驱动设计实战(六)-理解领域事件(Domain Event)(中)
1052 0
|
架构师
DDD建模系列(一)
DDD建模系列(一)
|
12月前
|
存储 关系型数据库 MySQL
MySQL主从复制原理和使用
本文介绍了MySQL主从复制的基本概念、原理及其实现方法,详细讲解了一主两从的架构设计,以及三种常见的复制模式(全同步、异步、半同步)的特点与适用场景。此外,文章还提供了Spring Boot环境下配置主从复制的具体代码示例,包括数据源配置、上下文切换、路由实现及切面编程等内容,帮助读者理解如何在实际项目中实现数据库的读写分离。
1212 1
MySQL主从复制原理和使用
|
Java API 项目管理
Java一分钟之-Gradle插件开发:自定义构建逻辑
【6月更文挑战第5天】Gradle插件开发详解:从入门到发布。文章介绍如何创建自定义插件,强调依赖管理、任务命名和配置阶段的理解。示例代码展示插件实现及避免常见问题的方法。最后,讨论插件的发布与共享,助你提升构建效率并贡献于开发者社区。动手实践,打造强大Gradle插件!
285 3
|
存储 安全 Java
Java的泛型与容器
Java的泛型与容器
112 2

热门文章

最新文章