使用DDD开发电商系统中的分类模块

使用DDD开发电商系统中的分类模块。

我们将讨论电商系统中的两个商品分类,分别是:电商系统类目的商品分类和商家自定义的商品分类。
并使用领域模型来实现这两个分类。

##假设业务

  • case 1:为了更方便的查找商品,给电商系统开发一套商品类目的分类,然后按照商品类目进行分类商品。
  • case 2:商家店铺也要支持商品分类,只不过可以让商家自定义商品分类。
  • 商品分类还要支持层级关系。比如说男鞋是一个类目,男鞋又可以分为豆豆鞋、老北京布鞋等等。
    此时有个精神小伙想买双豆豆鞋摇花手,他只需选择男鞋,然后选择豆豆鞋子类目就可以查找很多款式的豆豆鞋了。

划分子域

在划分子域之前,我想跟你解释下什么是领域驱动设计?

就拿我们具体的例子来讲,电商系统就是一个领域(domain),根据电商系统的业务驱动下而设计出来的一套模型就叫领域驱动设计。
此时如果我们继续下钻到电商系统内部又会发现更多的域,比如说客户店铺订单类目等等。
这些单个域仅仅反映了整个电商系统的一部分,所以我们把这些域叫做子域(sub domain)。

根据上面我们要开发的这两个商品分类可以看出他们分别组织在类目(catalog)子域和店铺(store)子域里。

通用语言

提到语言大家都不会陌生,因为你每天都需要用他来交流。
所以为了使客户、领域专家、开发人员方便交流,我们需要形成一套语言,
这样的能使一个团队进行无障碍交流的语言就是通用语言。

就比如电商系统中的通用语言有:
电商系统(mall)、目录子域(catalog)、店铺子域(store)、
商品类目(category)、店铺自定义的商品分类(custom collection)。

拆分模块

如今我们一提到开发电商系统,就是分布式、可扩展性强、可伸缩性强、高并发等等。
所以我们把目录(catalog)和店铺(store)这两个子域拆分成两个子系统。
然后再在每个子系统内部我们再划分模块,首先我们要开发的是领域模块,
然后为了让领域模块适配到某个端口,我们再为具体的端口开发适配模块。
比如说为了让catalog模块适配restful,我们就开发一个catalog-rest模块、
适配到rpc,就再开发一个catalog-rpc模块。

具体拆分细节如下图:

使用DDD开发电商系统中的分类模块_第1张图片

此UML类图是来自于我们即将开源的mall foundry的电商(mall)系统。
为了方便讲解我简化了类图,只保留了我们现在所关注的业务。

此时我还想说一下创建mall foundry的最初目的是为开发电商系统提供一站式的解决方案。
mall foundry是一个开箱即用、可插拔的组件库。
既可以单体运行,又可以分布式运行,而且还可以内嵌到现有的系统内。

项目结构

我们根据上面的设计图创建了如下项目:

使用DDD开发电商系统中的分类模块_第2张图片

此项目代码也是从mall foundry的项目中复制过来的,同时也做了简化。

商铺子系统(store subsystem)

首先我们先分析讲解商铺(store)子域中的领域业务,此子系统中只有一个领域模型,自定义商品分类(CustomCollection),
由于商铺的自定义商品分类不会很多,对于性能的影响不大,所以我们在查看某个商铺的商品分类时,将一次性加载全部分类。
所以我们在CustomCollection类中添加一对多的自关联children属性。代码如下:

CustomCollection

@Getter
@Setter
@NoArgsConstructor
@JsonPropertyOrder({"id", "name", "position", "children"})
@Entity
@Table(name = "store_custom_collection")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type_")
public class CustomCollection implements Comparable<CustomCollection> {

    @JsonIgnore
    private StoreId storeId;

    @Id
    @GeneratedValue
    @Column(name = "id_")
    private Integer id;

    @Column(name = "name_")
    private String name;

    @Column(name = "position_")
    private Integer position;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "parent_id_")
    private List<ChildCustomCollection> children = new ArrayList<>();

    public CustomCollection(String name) {
        this.setName(name);
    }

    public CustomCollection(StoreId storeId, String name) {
        this.setStoreId(storeId);
        this.setName(name);
    }

    @Embedded
    public StoreId getStoreId() {
        return storeId;
    }

    public ChildCustomCollection createChildCollection(String name) {
        return new ChildCustomCollection(this, name);
    }

    public void addChildCollection(ChildCustomCollection collection) {
        collection.setPosition(Integer.MAX_VALUE);
        collection.setParent(this);
        this.getChildren().add(collection);
        CustomCollectionPositions.sort(this.getChildren());
    }

    public void removeChildCollection(ChildCustomCollection collection) {
        this.getChildren().remove(collection);
    }

    public void clearChildCollections() {
        this.getChildren().clear();
    }
}

TopCustomCollection

@NoArgsConstructor
@Entity
@DiscriminatorValue("top_custom_collection")
public class TopCustomCollection extends CustomCollection {

    public TopCustomCollection(StoreId storeId, String name) {
        super(storeId, name);
    }
}

ChildCustomCollection

@NoArgsConstructor
@Entity
@DiscriminatorValue("child_custom_collection")
public class ChildCustomCollection extends CustomCollection {

    @JsonIgnore
    @ManyToOne
    private CustomCollection parent;

    public ChildCustomCollection(String name) {
        super(name);
    }

    public ChildCustomCollection(CustomCollection parent, String name) {
        super(parent.getStoreId(), name);
        this.setParent(parent);
    }

    @Embedded
    @Override
    public StoreId getStoreId() {
        return Objects.nonNull(this.getParent()) ? this.getParent().getStoreId() : super.getStoreId();
    }
}

为了更透彻的讲解下面的内容,我要说一下数据模型与领域模型的区别。

一提到数据模型你可能想到了E-R模型也有可能是数据库中的表,而对与这种模型来说他只是描述了静态数据,而无行为。
没有行为会怎么呢?

我们就拿为可以为商品分类添加一个子分类这个业务来说,使用数据模型就会创建一个CustomCollection对象,
然后设置父标识(parent id),最后使用SQL插入到表中。
这并没去体现到面向对象,而只是过程化的插入数据。代码如下:

    @Transactional
    @Rollback
    @Test
    public void testAddCollectionUseDataModel() {
        ChildCustomCollection collection = new ChildCustomCollection();
        collection.setStoreId(new StoreId("mall-foundry"));
        collection.setParent(null); // collection.setParentId("parent id");
        collection.setName("data model collection");
        customCollectionRepository.save(collection);
    }

接下来我们使用领域模型来开发这个同样的业务。
给商品分类添加子分类,肯定是要通过商品分类对象添加子分类对象,
所以我们应该在CustomCollection这个类中写一个添加子分类(addChildCollection)的方法,在方法内部封装了添加子对象的细节。
最后我们使用CustomCollection对象调用addChildCollection这个方法来添加子分类。代码如下:

    @Transactional
    @Rollback
    @Test
    public void testAddCollectionUseDomainModel() {
        CustomCollection collection = customCollectionRepository.findById(1);
        collection.addChildCollection(new ChildCustomCollection("child custom collection"));
    }

这样来看领域模型是不是更符合业务需求呢?
有关商家子域中商品分类的其他细节你可以查看示例代码。

###目录子系统(catalog subsystem)

目录(catalog)子域中的商品分类(category)和
商家(store)子域中的商品分类(custom collection)业务差不多。
只不过前者是整个系统的商品类目分类,后者只限定在商家(store)子域中。

因为目录子域商品分类(category)是服务整个系统的,每个顶级目录(top category)下的子目录有很多,
如果一次性获取全部的目录肯定会影响性能,所以我们把顶级目录和子目录分两次加载。
也就是说,先获取顶级目录集合,然后当你选择某个顶级目录时再次获取子目录集合。

首先我们使用的ORMspring-data-jpa,所以我们把@OneToMany注解添加到children属性上,
@OneToMany默认就是懒加载(lazy)机制所以我们不需要做过多的处理。

在表现层我们使用spring mvc@RestController发布RESTful接口,数据格式为json
为了在响应顶层目录(top category)时忽略children属性,
我们为Category派生类TopCategorychildren属性添加了@JsonIgnore注解。
代码如下:

Category

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "catalog_category")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type_")
public class Category implements Comparable<Category> {

    @Id
    @GeneratedValue
    @Column(name = "id_")
    private Integer id;

    @Column(name = "name_")
    private String name;

    @Column(name = "position_")
    private Integer position;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "parent_id_")
    private List<ChildCategory> children = new ArrayList<>();

    public Category(String name) {
        this.name = name;
    }

    public ChildCategory createChildCategory(String name) {
        return new ChildCategory(this, name);
    }

    public void addChildCategory(ChildCategory childCategory) {
        childCategory.setPosition(Integer.MAX_VALUE);
        childCategory.setParent(this);
        this.getChildren().add(childCategory);
        CategoryPositions.sort(this.getChildren());
    }

    public void removeChildCategory(ChildCategory childCategory) {
        this.getChildren().remove(childCategory);
    }

    public void clearChildCategories() {
        this.getChildren().clear();
    }
}

TopCategory

@Entity
@DiscriminatorValue("top_category")
@NoArgsConstructor
public class TopCategory extends Category {

    @JsonIgnore
    @Override
    public List<ChildCategory> getChildren() {
        return super.getChildren();
    }

    public TopCategory(String name) {
        super(name);
        this.setPosition(Integer.MAX_VALUE);
    }
}

ChildCategory

@Getter
@Setter
@NoArgsConstructor
@Entity
@DiscriminatorValue("child_category")
public class ChildCategory extends Category {

    @JsonIgnore
    @ManyToOne
    private Category parent;

    public ChildCategory(String name) {
        super(name);
    }

    public ChildCategory(Category parent, String name) {
        super(name);
        this.setParent(parent);
    }
}

有关目录子域中商品分类的更多细节你可以查看示例代码。

总结

此篇文章通过一个简单的例子来讲解如何使用领域模型开发业务。

可能此时你觉得这并没有什么特别之处,只是将一些方法操作移动到了模型之上,
我使用数据模型一样可以完成这样的功能。

对于后端开发人员面对一个需求时,我们不只是为了实现效果而是为了实现效果背后所隐含的业务。
当你是为了解决领域业务而不只是为了展示效果编码的时候,你就能感受到领域模型存在的意义了。
此时你就能体会到之前的数据模型(data model)只是为了传输而存在的,并没有表现出领域业务。

结束

上述文章仅代表我个人思考,禁止转载,如有误人子弟之处,请您指出。

如有问题请加QQ群讨论:

使用DDD开发电商系统中的分类模块_第3张图片

你可能感兴趣的:(领域驱动设计)