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

一、背景

最近公司准备进行业务组件的开发,正好我也准备讲一下《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,Value.
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识别其类型那代码量可能会更多一些。

更多优质DDD内容请关注公众号:
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值