DDD-领域驱动设计示例

作者: zhackertop | 来源:发表于2018-08-20 14:50 被阅读194次

    一、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());
        }
      }
    }
    

    参考

    相关文章

      网友评论

        本文标题:DDD-领域驱动设计示例

        本文链接:https://www.haomeiwen.com/subject/hwhliftx.html