Spring Boot+JPA实现DDD(二)

从聚合根开始

上一篇已经把业务需求描述清楚了,现在我们来实现它。

环境

  • JDK1.8+
  • Maven3.5+
  • Mysql8.0
  • Intellij Idea lombok 插件(注意安装插件要给Idea配置代理,否则装不上)
  1. 新建Spring Boot工程
    start.spring.io新建一个productcenter的项目。注意右边勾选lombok,Spring Data JPA和Mysql Driver。点击“GENERATE”生成项目。

  2. 新建包结构
    我们知道DDD有四层架构。

  • 用户接口层
  • 应用层
  • 领域层
  • 基础设施层
    按照这个结构我们分别建4个包: ui, application, domain, infrastructure
  1. 实现模型
    没有表结构突然不知道从哪里开始了?以前因为已经有表结构了,我们一开始用工具自动生成entity,然后就开始写controller,service,dao了。
    DDD是以领域为核心的,领域里的模型是稳定的,不管外部怎么变化,我们的模型是保持不变的。注意这里说的“稳定”、“不变”是指项目上线后不变,在开发阶段,模型是要不断优化调整的。所以我们就从模型开始。当然如果你的项目要先跟别人定好接口再开发,那你可以先从controller开始。然后构建模型。

在domain下新建 model.product.Product类。

/**
 * 商品聚合根
 */
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "product_no", length = 32, nullable = false, unique = true)
    private String productNo;
    @Column(name = "name", length = 64, nullable = false)
    private String name;
    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    @Column(name = "category_id", nullable = false)
    private Integer categoryId;

    @Column(name = "product_status", nullable = false)
    private Integer productStatus;
    @Column(name = "remark", length = 256)
    private String remark;

    @Column(name = "allow_across_category", nullable = false)
    private Boolean allowAcrossCategory;

    public Product(Long id, String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, 
                                              String remark, Boolean allowAcrossCategory) {
        this.id = id;
        this.productNo = productNo;
        this.name = name;
        this.price = price;
        this.categoryId = categoryId;
        this.productStatus = productStatus;
        this.remark = remark;
        this.allowAcrossCategory = allowAcrossCategory;
    }

}

类的属性用JPA的@Column跟db表的字段对应起来,并且类的属性跟业务密切相关。商品有名称,价格,类目,状态,是否允许跨类目,备注字段。
此外,除了一个自增的主键,商品应该还一个唯一的产品编码。这个唯一的产品编码就是业务主键,跟外部交互的时候都使用这个业务主键。这至少有3个好处:

  • 对前端不会暴露我们的实现
  • 如果有一天需要迁移数据的时候,因为业务主键是稳定的,很好迁移。而物理主键是会变的,迁移到另一张表可能还会有主键冲突,到时候就很难受。
  • 业务主键是可读的,并且其本身包含了一些有用信息。

因为hibernate需要entity提供一个无参构造函数,我们用lombok注解@NoArgsConstructor(access = AccessLevel.PROTECTED)。注意到,这里的访问权限给的是protected,这样是防止外部直接new Product()创建一个空的商品。

现在观察一下我们的有参数构造函数访问权限是public。这意味着,其他地方可以随意的创建一个商品。问题是他们知道如何正确的创建一个商品吗?
也许你会说,我们把创建商品需要的业务规则都放在这个构造函数里不就行了吗? 行是行,就是不灵活了。假如某一天我们想返回Product的一个子类怎么办?

所以我们应该提供一个工厂方法。由这个工厂方法统一创建商品。 双击构造函数名称,右击鼠标 Refactor >> Replace Constructor with Factory Method
输入工厂方法名of。 你会看到,idea自动把构造函数变成了私有的方法:

private Product(Long id, String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, String remark,  
                                           Boolean allowAcrossCategory) {
        this.id = id;
        this.productNo = productNo;
        this.name = name;
        this.price = price;
        this.categoryId = categoryId;
        this.productStatus = productStatus;
        this.remark = remark;
        this.allowAcrossCategory = allowAcrossCategory;
}

public static Product of(Long id, String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, 
                                                    String remark, Boolean allowAcrossCategory) {
    return new Product(id, productNo, name, price, categoryId, productStatus, remark, allowAcrossCategory);
}

再看看代码,好像有点“坏味道”,既然已经用了lombok,为什么还要自己写一个构造函数呢。
把有参构造函数删掉, 在类上加一个 @AllArgsConstructor(access = AccessLevel.PRIVATE)

有参数构造函数的访问权限是private。 第一次见到这个你可能会觉得不可思议,因为以前你从来没想过要把构造函数变成私有的。
不仅如此,setter和getter也是随便给。这是不对的,DDD的代码要严格控制访问权限,这样才能最大程度上保证模型的稳定。不然就会出现一个属性的值不知道在什么地方被改了,你却不知道的情况。一旦出现这样的bug,简直就是灾难。

虽然实体是可被修改的,但不代表所有属性都随便调用setter轻轻松松就改掉了。
如果确实需要修改某个属性,请提供一个具体的方法,比如changeProductStatus,这个方法跟业务上也应该有对应关系,否则就没必要单独写一个方法。
这才叫封装嘛,你说是不是?

没有setter和getter,hibernate还能实现持久化吗?
以前的hibernate要求entity必须有setter和getter,现在不需要了。

工厂方法也有点问题。主键id是自动生成的,怎么能让程序传进来呢。所以工厂方法删除id这个参数,在调用Product有参构造函数的时候id传一个null。

Product类最后变成这个样子:

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Product implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "product_no", length = 32, nullable = false, unique = true)
    private String productNo;
    @Column(name = "name", length = 64, nullable = false)
    private String name;
    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    @Column(name = "category_id", nullable = false)
    private Integer categoryId;

    @Column(name = "product_status", nullable = false)
    private Integer productStatus;
    @Column(name = "remark", length = 256)
    private String remark;

    @Column(name = "allow_across_category", nullable = false)
    private Boolean allowAcrossCategory;

    public static Product of(String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, 
                                               String remark, Boolean allowAcrossCategory) {
        return new Product(null, productNo, name, price, categoryId, productStatus, remark, allowAcrossCategory);
    }
}
  1. 启动项目
    启动的时候会报如下错误
    Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured
    因为我们还没配置过数据库连接。现在来配置它。application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/product_center? 
  useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=<username>
spring.datasource.password=<password>
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=create
# note that "spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect" was deprecated
spring.hibernate.dialect.storage_engine=innodb

在mysql里创建一个名为product_center的库,再次启动项目。hibernate自动为我们生成了一个product表:

  1. 复写equals和hashCode方法(重要)
    添加guava包:
<properties>
    <guava.version>29.0-jre</guava.version>
</properties>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>${guava.version}</version>
</dependency>

Alt+Insert,选择 equals() and hashCode(),Template选择Objects.equals and hashCode(Guava),点击“下一步”,Member选择productNo:String,下一步,Finish。
生成的equals和hashCode方法如下:

@Override
public boolean equals(Object o) {
	if (this == o) return true;
	if (o == null || getClass() != o.getClass()) return false;
	Product product = (Product) o;
	return Objects.equal(productNo, product.productNo);
}

@Override
public int hashCode() {
	return Objects.hashCode(productNo);
}

ProductproductNo是唯一的,两个实体,只要这个字段相同,就认为是同一个实体。

源码下载: productcenter2.zip

文章有点长了,下一篇我们继续。

原文地址:https://www.cnblogs.com/ahau10/p/13516707.html