写点什么

用一个极致简单的场景演练领域建模

作者:小帅哥
  • 2021 年 11 月 15 日
  • 本文字数:4057 字

    阅读完需:约 13 分钟

一、背景

最近公司准备进行业务组件的开发,正好我也准备讲一下《DDD 理论与实践》的技术分享,在进行通用业务组件设计方案的时候发现了一个特别容易理解也很容易讲明白的案例,这里专门记录一下,分享给大家。

二、key-value 对象

2.1 KV 对象的基本模型

很多项目都会用到 KV 对象,但是对于 KV 对象模型的使用各有不同,如 Sql 表本身的表 k-v 存储,严格来说是 K-VList.那么最纯粹的 k-v 就是 Map,如 Map<K,V>。从 KV 的基本模型来看 K 是我知道你是谁,V 是我了解你是谁。当然 KV 对象的这种模型也叫键值对模型。K 对应键,V 对应值。也有很多底层框架会用键值对模型来简化本身对于数据模型的操作。这是从现实中抽象出的一个最纯粹的模型,当然也最简单。

2.2 KV 对象的使用场景

KV 对象的使用场景有很多,这里可以举一些例子


  1. MySql-column 的 K-VList 模型

  2. 枚举,只有 KV 两个到多个字段的模型

  3. Map 的数据模型

  4. 唯一性模型,如确定 Key 的取值规则之后即可确定唯一性模型

  5. 由 KV 扩展出的业务配置对象,比如三元组组成的模型(id-k-v,k-v-desc),或者说基于配置的几条数据库记录。

  6. 字典,字典实际上也是典型的 KV 对象模型,当然其专业的叫法应该算是倒排索引,算是 KV 的变形 V-K 模型。

  7. KV 对象的范型应用如 Key<K>,Value<V>.

2.3 KV 对象在领域建模的应用

在领域驱动建模中领域对象是分实体和值对象两种,实体里的元数据信息其实是一系列的 k-v 集合。但是对于 KV 本身对象而言,KV 业务行为或者业务属性表现的并不多。但是实际上 KV 在值对象的应用是非常广泛的,适配性也非常好。所以在这里我个人觉得 KV 对象其实可以算是值对象的一种类型抽象。

2.4 KV 对象的 Java 范型类

  1. 我们简单看一下基于 KV 对象的范型类是怎么样的。


package com.coderman.utils.kvpair;
import java.util.Objects;
/** * description: KVPair <br> * date: 2020/7/18 0:29 <br> * author: coderman <br> * version: 1.0 <br> * k-v键值对对象 */public class KVPair<K,V> { public KVPair(){
} public KVPair(K k,V v){ this.k = k; this.v = v; } protected K k; protected V v;
public K getK() { return k; }
public void setK(K k) { this.k = k; }
public V getV() { return v; }
public void setV(V v) { this.v = v; }
//提供构建k-v实例的静态方法 public static <K, V> KVPair<K, V> build(K k, V v) { return new KVPair<>(k, v); }
//提供判断k-v对象是否相同的能力 @Override public boolean equals(Object o) { if (this == o){ return true; } if (o == null || getClass() != o.getClass()) { return false; } KVPair<?, ?> kvPair = (KVPair<?, ?>) o; return Objects.equals(k, kvPair.k) && Objects.equals(v, kvPair.v); }
@Override public int hashCode() { return Objects.hash(k, v); }}
复制代码


  1. k-v 对象的扩展 k-v-p


package com.coderman.utils.kvpair;
import java.util.Objects;
/** * description: KVParentPair <br> * date: 2020/9/6 16:09 <br> * author: coderman <br> * version: 1.0 <br> * 具有父子级关系的三元组 */public class KVParentPair<K,V,P> extends KVPair<K,V> { private P p; public KVParentPair(){}
public KVParentPair(K k,V v ,P p){ super(k,v); this.p = p; }
public P getP() { return p; }
public void setP(P p) { this.p = p; }
public static <K, V> KVPair<K, V> build(K k, V v) { return new KVPair<>(k, v); }
@Override public boolean equals(Object o) { if (this == o){ return true; } if (o == null || getClass() != o.getClass()) { return false; } KVParentPair<?, ?, ?> kvParentPair = (KVParentPair<?, ?, ?>) o; return Objects.equals(kvParentPair.k, super.k) && Objects.equals(kvParentPair.v, super.v) && Objects.equals(kvParentPair.p,this.p); }
@Override public int hashCode() { return Objects.hash(super.k, super.v,p); }}
复制代码

三、key-value 对象的业务场景建模

这里我们对 key-value 对象做一次业务建模,模拟现实中存在的一些场景,并尝试得到一个相对通用的 kv 业务对象和业务服务类。

3.1 在其他对象中当作值对象(只有 k-v)

这里如果是在其他对象中当作值对象的话,我们对上面的 KVPair 对象进行 UML 类图建模,得到 KVPair 基本业务模型:



这里我们对 KV 对象做了一个初步的简单处理,目前有了两个方法表达了这个对象的初步能力。那假设现在我们遇到了后面几种情况如何基于这个对象进行扩展呢,从领域建模的角度来说我们看一下如何基于这个对象构建更丰富的能力。

3.2 在数据中存在一定意义(id-k-v)

现在我们要把这种 KV 类的对象存入数据库,需要具有读写持久化的能力。那么我们需要对上述的 KV 对象做一个改变,并增加对其操作的 Service 类。这里我们不再加入创建人创建时间等其他字段,着重突出键值对的业务能力。



3.3 支持 k-v 可分组的特性

现在假设领导要求继续在业务场景上增加键值对象类的应用。有如下两种场景。

1.按 k 的属性集合比如我们需要对 k 本身按某一业务场景做分组或者说对某一业务对象打散存储。那需要对上面的 KVPairBO 做进一步的扩展,此时我们的 KVPairBO 就会变成下面的样子,如下图:


2.另外将 k-v 当作某一外部记录的集合通过第一种情况我们知道某些 key 是具有相似性的,因此我们加了一个属性作为分组,此时我们的 KVPairService 能展示出的方法能力也多了一些,下面我们看一下,如果 k-v 类在数据库中存在并且属于某张表或者某个业务对象的扩展字段信息,比如商品的扩展属性信息等等。那我们通过 groupKey 来支持这种能力显然不是很合适。因为如果一个对象需要扩展多个分组的 k-v 结构的话这个字段就有点力不从心了。因此我们看一下增加两个字段是否能解决这个问题:



3.4 支持 k-v 之间具有父子级的关系

现在我们已经从 3.3 中扩展了两个维度的 K-V 关系,那么我们来看一下如何用 KVPair 来构建组织树这种树形结构。由于 groupKey,relationKey,Long relationId 这俩是用来进行特定场景的扩展,如果用这些来构建组织树的话也不是不行,但是代码上会变得复杂而且也无法表达树形结构的含义。因此我们来看一下下面的类图是如何解决这个问题的:


3.5 支持 value 是复杂业务对象--json 化

现在我们对 key 的维度扩展的差不多了,现在我们来看一下对 value 的支持。假设现在遇到一些业务规则或者配置类对象,也希望使用上述 KVPair 的结构进行读写管理。但是唯一的区别就是要求支持 value 是 json 串可以序列化和反序列化。对于 value 是字符串的场景天然支持 json 字符串,但是要求可以序列化和反序列化,那如何支持呢。我们看看如何在不增加属性的基础上来解决这个问题,现在看一下下面的类图:


可以看到我们在 KVPairBO 上仅仅增加了一个类即可实现该需求,对于序列化来说直接走 build 的方法即可,范型仍然是 K,String 类的。如果要做的完全支持此类需求则可以增加一个范型参数来代表要反序列化的对象,此时对象内部可以完全掌控 json 的序列化反序列化的需求。

3.6 支持对 v 进行解析得到特定的数据如 count,size

到现在为止你可能觉得有点意思或者没啥特色,那我们继续来玩点花活。假设我们知道 value 的大概内容或者格式,因此此时的范型已经无法完全满足上面的全部场景对于 KV 的定义了,然而增加范型参数也会变得极其复杂。这里我们可以对于 value 的解析可以更近一步,比如除了对于 JSON 字符串类型的解析序列化之外我们还可以脱离 KV 范型本身的约束来得到具体的值,举例如果是 json 数组,或者是普通字符串拼接的。那我们依然可以继续来增强这个对象的能力,看下图:



3.7 value 类型问题

从上面的扩展来看,此时的 KV 对象已经有点面目全非的意思了。一个导致其变得复杂的原因是 KV 都是范型,遇到 V 的范型无法约束的结构时往往对 V 的定义值和实际值的应用超出了控制范围。所以这里可以对 KVPairBO 对象再加一个 String varType 的字段来标示 V 的大概类型,这样比较好处理。完整 UML 图就不加了,请读者自行脑补。

3.8 对 KV 的反转

如果此时你已经有点惊叹了,那我再来一个小小的扩展,假设现在我们已经应用了这个数据结构做了一些业务功能的实现。现在有个需求就是需要导入数据,我的数据是 excel 里面是 value,我需要匹配到对应的 Key。但是实际上内存里存的可能都是 KV,或者 Map<K,V>。那我如何能不遍历快速找到 K 呢?

3.9 按 Key 排序

从上面 UML 类图中我们可以继续扩展一下就是说当遇到需要对 List<KV>进行按 K 或者 Value 排序的时候我们需要对 KVPairBO 实现支持对象排序比较的接口,这里不再深入。


四、总结

按领域建模的思路从最简单的 KV 开始我们经历了很多场景,这就是一个工程模块为啥到后面越来越大大到无法控制的一个缩影。就上面的结构到这里即使你拿去应用依然可能无法掌控其复杂度,究其原因在于除了 BO 对象层面的应用也跟持久化有点关系。另外一方面对于上述的场景都放在同一个类和一个业务模型下肯定有一些问题。这就违背了 Java 代码设计层面的单一职责原则。其实对于 KVPairBO 类来说也还好,但是其 KVPairService 确是我使用 DDD 的独立类模式来给大家演示的。所以这里也体现了如果没有定义接口你会知道独立类给你带来的伤害有多大。那下面我们知道上面的问题所在了,其实就可以针对性的做几个服务接口,然后做不同场景的实现,此时就是对 KVPairService 进行拆分,让各自的实现独立负责,KVPairService 可能只负责路由或者说做一些公共逻辑处理等等。那到这里的话其实这个案例已经不简单了,如果需要对 Value 识别其类型那代码量可能会更多一些。

五、戳我交流

公众号:[神帅的架构实战]

发布于: 2021 年 11 月 15 日阅读数: 8
用户头像

小帅哥

关注

我是神帅,混迹于大小厂刷实战经验。 2020.08.08 加入

是个宝爸,热爱生活做饭,在企业服务领域和电商领域均有涉猎,最近一直在研究DDD和低代码领域,对后端微服务业务平台架构的实践和发展比较感兴趣。 公众号:神帅的架构实战

评论

发布
暂无评论
用一个极致简单的场景演练领域建模