從聚合根開始
上一篇已經把業務需求描述清楚了,現在我們來實現它。
環境
- 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
文章有點長了,下一篇我們繼續。