首页 > 其他分享 >DDD-领域驱动设计示例

DDD-领域驱动设计示例

时间:2024-04-22 11:37:40浏览次数:22  
标签:示例 spuId spu shopId 驱动 message Spu public DDD

一、DDD概述

  • DDD,即领域驱动设计,核心是不断提炼通用语言并用于与领域专家等团队所有成员交流,并用代码来表达出一个与通用语言一致的领域模型

  • 通用语言:通过团队交流达成共识的能够简单清晰准确传递业务规则的语言(可以是文字、图片等)

  • 领域:软件系统要解决的问题域,是有边界的。领域一般包含多个子域,子域根据其功能划分为核心域、通用域、支撑域。

  • 限界上下文:描述领域边界,一个限界上下文可能包含多个子域,但一般实践上都以一对一为好。应用单元和部署单元一般也与限界上下文一致。


      领域与限界上下文.png
  • 限界上下文映射:多个上下文之间如何进展系统交互集成。


      上下文映射.png
  • 领域模型:对我们软件系统中要解决问题的抽象表达(解决方案)。模型一般在一个限界上下文中有效。

    • 模块
    • 聚合根
    • 实体
    • 值对象
    • 领域事件
    • 仓储定义
    • 领域服务
    • 工厂
    • 限界上下文映射的反腐层定义


        限界上下文中的领域模型.png
  • 领域实现:

    • 领域模型
    • 应用服务
    • 基础设施
      • 服务暴露
      • 仓储实现
      • 反腐层实现
  • 实践步骤为:

    • 找到子域
    • 识别核心域、通用域、支撑域
    • 确定限界上下文映射
    • 在每个子域内设计领域模型
    • 实现领域模型和应用

二、示例需求分析

要实现多规格商品的创建和查询。

  spu domain

Spu相关操作如下:

@RestController
@RequestMapping("/v1/spu")
public class SpuRestApi {
  
  //spu创建
  @PostMapping("/create")
  public Result<Long> create(@RequestBody SpuCreateParam param){

  }

  //spu详情
  @GetMapping("/detail")
  public Result<SpuVO> findSpuById(Long shopId, Long spuId){
      
  }
}

SpuCreateParam的定义如下:
其中spuNo,skuNo,barCodes等要求唯一

{
  "shopId": 0, //店铺ID
  "categoryId": 0,//分类ID
  "unitId": 0,//单位ID
  "name": "string",//SPU名称,长度20,不能为空
  "spuNo": "string",//SPU编码,不可变更,用于各系统间传递
  "barCodes": [//SPU条码列表,最多10个,用于搜索
    "string"
  ],
  "photoTuple": {//图片列表,最多10张
    "photos": [
      {
        "url": "string" //必须为合法url,每个url长度最大为120
      }
    ]
  },
  "specDefineTuple": {//规格定义,项与值都不能重复,相对顺序用于sku列表的排序
    "defines": [//规格项列表,如【颜色+尺寸】
      {
        "key": "string",//规格项,如颜色
        "values": [//规格值列表,如红色、白色等
          "string"
        ]
      }
    ]
  },
  "skus": [//SKU列表,要符合规格定义的笛卡尔积
    {
      "skuNo": "string",//SKU编码
      "barCodes": [//SKU条码,用于SKU维度搜索,最多10个
        "string"
      ],
      "retailPrice": 0,//零售价,分,最大为 100w*100
      "specTuple": {//与规格定义笛卡尔积中每一个组合对应,如【红色 + 20号】
        "specs": [
          {
            "key": "string", //规格项,如颜色
            "value": "string" //规格值,如红色
          }
        ]
      }
    }
  ]
}

三、分层架构 + 面向过程设计

特征:

  • 包划分上以功能为准,如所有model放一个包,所有service放另一个
  • 服务依赖:SpuApi -> SpuService -> SpuMapper -> Mybatis
  • 创建时数据流向:SpuSaveParam -> Spu -> table
  • 查询时数据流向:SpuVO <- Spu <- table
  • 整个SpuService只包含简单的CRUD操作,尤其是更新操作,一般倾向于只有一个万能的Update。从方法名称,你看不出任何的业务含义。
  • SpuService:一个服务方法几乎包含了所有的逻辑,负责校验、获取外部信息、组装、转换SpuSaveParam为Spu、并调用SpuMapper保存到数据库。
  • Spu为失血模型,只包含字段,没有get/set之外的方法,Spu与table的字段几乎一一对应。

优缺点:

  • 在逻辑很简单场景下,crud迭代最快,面向过程与人类思考的方式相近。
  • 在复杂场景下,如spu创建涉及大量的校验组装等,很快SpuService.save方法就会过于庞大。另外,有大量的校验逻辑,在更新场景下是可以复用的。

四、pipeline设计

  checker pipeline

spu的校验可以根据spu的内聚信息块划分成多个checker,然后将多个checker组合成一个pipeline流,从而可以更好的重用,并快速应对新增的校验(加个checker就行了)。
另外,获取外部信息,如category、unit等,也可以用rxjava等并发去做,以加快速度。

缺点:

  • 但是pipeline要求设计出一个好的context,用于上下文传递,一般会出现context的腐化。
  • 另外,service的主逻辑不清晰,读代码的成本变高。

五、六边形架构 + 面向对象设计

  六边形架构层级

示例实现的github地址

特征:

  • 采用分治法,将数据、约束、行为等划分到最能表达它的领域模型中。
  • 包划分上以业务模块为准,同业务的identity、valueObject、event、repository、service等放在一个包下。
  • SpuAppService:为应用服务,只是调用领域服务和仓储等来串流程,不包含业务逻辑,如校验等。
  • 领域服务:本实例中没有领域服务,如果有的话,会定义为SpuXxxService(Xxx指明业务操作)
  • Spu: 包含字段和行为,如校验在构造和set时内置,方法体现业务操作如changeName,不是单一的update动作。
  • SpuRepo:定义了仓储的操作,实现在infra中基于mybatis等
  • MybatisSpuRepo: 实现
  • SpuMapper:基于mybatis访问数据库

具体包划分如下:

  • domain
    • shop
    • category
    • unit
    • spu
      • sku
      • spec
      • code
      • event
      • Spu
      • SpuRepo
      • SpuService
  • infra
    • repo
    • proxy

具体代码实现如下:

class SpuAppService{ //应用服务

  @Transactional
  public SpuId save(SpuCreateParam param){
    
    ShopId shopId = new ShopId(param.getShopId());
    
    //调用外部服务获取关联信息,并验证了关联信息的合法性
    Category category = categoryService.findById(param.getShopId(), param.getCategoryId());
    Unit unit  = unitService.findById(param.getShopId(), param.getUnitId());
    
    //调用Repo生成ID,后续流程中很有可能需要它
    SpuId spuId = spuRepo.nextId();
    
    //SpuNo构造时验证参数的合法性,不包含特殊字符,不会超长等
    //lockSpuNo, 用于保证编码的唯一性,注意要实现为可重入锁
    SpuNo spuNo = codeLockService.lockSpuNo(new SpuNo(shopId, spuId, param.getSpuNo()));
    
    //与SpuNo相似
    SpuBarCodeTuple spuBarCodeTuple = codeLockService.lockSpuBarCodes(new SpuBarCodeTuple(shopId,spuId,param.getBarCodes()));
    
    //用于根据参数生成对应的sku列表
    List<Sku> skus = skuService.buildSkus(shopId, spuId, param.getSkus());
    
    Spu spu = Spu.builder()
        .shopId(shopId)
        .spuId(spuId)
        .no(spuNo)
        .barCodes(spuBarCodeTuple)
        .name(param.getName())
        .photoTuple(param.getPhotoTuple())
        .category(category)
        .unit(unit)
        .specDefineTuple(param.getSpecDefineTuple())
        .skus( skus) //构造时触发笛卡尔积相关校验
        .build(); //当实例化Spu时会调用法律时刻构造函数来校验以上各信息的约束条件
    
    //在本步骤前,spu和sku都未生成
    //spu是聚合根,其包含sku的实体的创建。
    //因为sku的规格组与spu的规格定义是有对应约束的。
    spuRepo.save(spu);
    
    return spuId;
  }
}
class MybatisSpuRepo implements SpuRepo{//仓储实现
  @Override
  public void save(Spu spu) {
    spuMapper.create(spu);
    skuMapper.batchCreate(spu.getSkuTuple().getSkus());
  }
}

@Mapper
public interface SpuMapper { //Mybatis实现数据库访问
  @Options(useGeneratedKeys = true, keyProperty = "id")
  @InsertProvider(type = SpuMapper.class, method = "createSql")
  void create(Spu spu);
  
  @Results(
      id = "spuDetail",
      value = {
          @Result(property = "shopId.id", column = "shop_id"),//复杂对象映射
          @Result(property = "barCodes", column = "bar_codes", typeHandler = SpuBarCodeTupleHandler.class)) //复杂对象JSON化为字符串
      }
  )
  @Select("select * from spu where shop_id = #{shopId} and spu_id = #{spuId}")
  Spu findById(@Param("shopId") Long shopId, @Param("spuId") Long spuId);
}
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Getter
@Setter(AccessLevel.PROTECTED)
@Accessors(chain = true)
@Builder
@Slf4j
public class Spu extends IdentifiedEntity {//实体,同时也是聚合根
  
  @NotNull(message = "商家不能为空")
  private ShopId shopId;
  
  @NotNull(message = "ID不能为空")
  private SpuId spuId;
  
  @NotNull(message = "名称不可为空")
  @Size(max = 100, min = 1, message = "名称字符数为1到100个字")
  private String name;
  
  @NotNull(message = "编码不能为空")
  private SpuNo no;
  
  //SpuBarCodeTuple内部保证其合法性,spu不用管理其细节,只要不为空,这个条码组就是合法的。
  @NotNull(message = "条码组不能为空")
  private SpuBarCodeTuple barCodes;
  
  @NotNull(message = "图片不能为空")
  private PhotoTuple photoTuple; 
  
  @NotNull(message = "分类不能为空")
  private CategoryId categoryId;
  
  //导航属性,可空,在某些需要的场景下去加载它
  //如Spu详情中应该包含,而spu列表中可以不存在
  private Category category;

  @NotNull(message = "单位不能为空")
  private UnitId unitId;
  
  private Unit unit;
  
  @NotNull(message = "规格定义不能为空")
  private SpecDefineTuple specDefineTuple;
  
  @ListDistinct(message = "规格不能重复")
  @Size(max = 600, message = "规格数最大不能超过600")
  private List<Sku> skus = new ArrayList<>();
  
  protected Spu(){ //用于使mybatis等框架能正常工作
  
  }
  
  public Spu(
      ShopId shopId,
      SpuId spuId,
      SpuNo no,
      SpuBarCodeTuple barCodes,
      String name,
      PhotoTuple photoTuple,
      Category category,
      Unit unit,
      SpecDefineTuple specDefineTuple,
      List<Sku> skuTuple) {
    this.shopId = shopId;
    this.spuId = spuId;
    this.name = name;
    this.no = no;
    this.barCodes= barCodes;
    this.photoTuple = photoTuple;
    this.category = category;
    this.categoryId = category.getCategoryId();
    this.unit = unit;
    this.unitId = unit.getUnitId();
    this.specDefineTuple = specDefineTuple;
    this.skus = skuTuple;
    
    //整合valiation框架,能基于上面定义的注解去校验,从而让校验以声明式写法来表述
    super.validate();
    
    //发布领域事件
    DomainEventPublisher.publish(new SpuCreatedEvent()
        .setShopId(shopId)
        .setSpuId(spuId)
    );
    
  }

  public Spu loadCategory(){ //加载分类
    if(this.category!=null){
      return this;
    }
    if(categoryId!=null){
      this.category = DomainRegistry.repo(CategoryRepo.class).findByShopIdAndId(shopId, categoryId);
    }
    return this;
  }
}

@ToString
@EqualsAndHashCode
@Getter
@Setter(AccessLevel.PROTECTED)
@Accessors(chain = true)
public class SpuBarCodeTuple extends AssertionConcern {
  
  @NotNull(message = "商家不能为空")
  private ShopId shopId;
  @NotNull(message = "spuID不能为空")
  private SpuId spuId;
  
  @NotNull(message = "条码列表不能为空")
  @ListStringSize(max = 20, message = "条码最多20个字符")
  @ListDistinct(message = "条码列表不能重复")
  @Size(max = 10, min = 0, message = "最多支持10个条码")
  List<String> codes = new ArrayList<>();
  
  public SpuBarCodeTuple(ShopId shopId, SpuId spuId, List<String> codes) {
    this.codes = codes;
    this.shopId = shopId;
    this.spuId = spuId;
    this.validate(); //触发约束校验
  }
  
  protected SpuBarCodeTuple() {
  }
}

@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { ListStringSize.ListStringSizeChecker.class })
public @interface ListStringSize { //自定义约束
  
  int min() default 0;
  int max() default Integer.MAX_VALUE;
  String message() default "列表元素大小不符合定义";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
  
  public static class ListStringSizeChecker implements ConstraintValidator<ListStringSize, List<String>> {
  
    ListStringSize annotation;
    
    @Override
    public void initialize(ListStringSize constraintAnnotation) {
      annotation = constraintAnnotation;
    }
  
    @Override
    public boolean isValid(List<String> objects, ConstraintValidatorContext constraintValidatorContext) {
      if(objects==null){
        return true;
      }
      return objects.stream().allMatch(s-> s.length()<=annotation.max() && s.length()>=annotation.min());
    }
  }
}


作者:zhackertop
链接:https://www.falvshike.com

标签:示例,spuId,spu,shopId,驱动,message,Spu,public,DDD
From: https://www.cnblogs.com/77cxw/p/18150277

相关文章

  • 5种搭建LLM服务的方法和代码示例
    在不断发展的大型语言模型(LLMs)领域中,用于支持这些模型的工具和技术正以与模型本身一样快的速度进步。在这篇文章中,我们将总结5种搭建开源大语言模型服务的方法,每种都附带详细的操作步骤,以及各自的优缺点。 https://avoid.overfit.cn/post/efad539d0969474987a7ff652f632d8e......
  • DDD领域驱动设计总结和C#代码示例
    DDD(领域驱动设计)是一种软件设计方法,它强调以业务领域为核心来驱动软件的设计和开发。DDD的设计初衷是为了解决复杂业务领域的设计和开发问题,它提供了一套丰富的概念和模式,帮助开发者更好地理解和建模业务领域,从而提高软件的质量和可维护性。一、DDP主要组成DDD的主要模式包括......
  • 在React中的函数组件和类组件——附带示例的对比
    在React中,创建组件有两种主要方式:函数组件和类组件。每种方式都有自己的语法和用例,尽管随着ReactHooks的引入,它们之间的差距已经显著缩小。但选择适当的组件类型对于构建高效和可维护的React应用程序仍然非常关键。在本文中,我们将探讨函数和类组件之间的基本区别,清楚地理解它们......
  • 基于事件驱动的测试框架ETS
    ETS(Event-drivenTestSystem)是一种基于事件驱动的测试框架,它可以用于自动化测试和软件质量保障。ETS的生命周期包括测试计划、测试设计、测试实现、测试执行和测试报告等阶段。本文将通过代码示例和图表的形式详细介绍ETS生命周期的各个阶段。测试计划在测试计划阶段,我们需要明......
  • NanoPi-NEO 全志H3移植Ubuntu 22.04 LTS、u-boot、Linux内核/内核树、mt7601u USB-Wi-
    前言想在NanoPi-NEO上开发屏幕驱动,但是看了下文件目录发现没有内核树,导致最基础的file_operations结构体都无法使用,于是寻找内核树安装方法。但官方提供的内核为4.14太旧了apt找不到对应的linux-source版本(其实后面发现不需要用apt,可以在kernel.org上下载,但反正都装了那就当学习......
  • 面向对象设计介绍和代码示例
    面向对象设计(Object-OrientedDesign,OOD)是一种软件设计范式,它使用对象来表示数据和方法。面向对象设计原则是指导软件开发的一系列最佳实践,旨在提高代码的可维护性、可扩展性和可重用性。以下是几个核心的面向对象设计原则,以及它们的解释、应用场景和代码示例:1.单一职责原则(Si......
  • 转载Using Domain-Driven Design(DDD)in Golang
    转载自:https://dev.to/stevensunflash/using-domain-driven-design-ddd-in-golang-3ee5UsingDomain-DrivenDesign(DDD)inGolang#go#ddd#redis#postgresDomain-DrivenDesignpatternisthetalkofthetowntoday.Domain-DrivenDesign(DDD)isanapproachtosoft......
  • 【STM32+HAL库】---- 驱动MAX30102心率血氧传感器
    硬件开发板:STM32F407VET6软件平台:cubemax+keil+VScode1MAX30102心率血氧传感器工作原理MAX30102传感器是一种集成了红外光源、光电检测器和信号处理电路的高度集成传感器,主要用于心率和血氧饱和度的测量。以下是MAX30102传感器的主要特点和工作原理:红外光源:MAX30102传感器......
  • Ubuntu 22.04 安装 Nvidia 驱动最方便安全的方式
    刚安装好的Ubuntu22.04没有N卡驱动,输入nvidia-smi,提示没有此程序并推荐到apt安装。但是,使用apt安装nvidia驱动会有极大概率出现启动黑屏和闪屏问题。不如进入开始菜单,找到“附加驱动”:此处展示了可用的Nvidia驱动,选择自己想要的版本安装,"tested"表明其经过测试,......
  • GDExtension的C++示例
    GDExtension的C++示例本文按照官方文档,进行c++的GDExtension​插件开发,主要进行文档进行复刻,同时对文档中未涉及步骤进行补充什么是GDExtension除了GDScript​和C#​这两种脚本语言外,Godot​引擎可以执行其他编程语言编写的代码。目前有两种方式实现:C++模块与GDExtension简单......