从聚合根开始
上一篇已经把业务需求描述清楚了,现在我们来实现它。
环境
- JDK1.8+
- Maven3.5+
- Mysql8.0
- Intellij Idea lombok 插件(注意安装插件要给Idea配置代理,否则装不上)
-
新建Spring Boot工程
start.spring.io新建一个productcenter的项目。注意右边勾选lombok,Spring Data JPA和Mysql Driver。点击“GENERATE”生成项目。
-
新建包结构
我们知道DDD有四层架构。
- 用户接口层
- 应用层
- 领域层
- 基础设施层
按照这个结构我们分别建4个包:ui
,application
,domain
,infrastructure
。
- 实现模型
没有表结构突然不知道从哪里开始了?以前因为已经有表结构了,我们一开始用工具自动生成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);
}
}
- 启动项目
启动的时候会报如下错误
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表:
- 复写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);
}
Product
的productNo
是唯一的,两个实体,只要这个字段相同,就认为是同一个实体。
源码下载: productcenter2.zip
文章有点长了,下一篇我们继续。