1. 程式人生 > 實用技巧 >Spring Boot+JPA實現DDD(二)

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,這個方法跟業務上也應該有對應關係,否則就沒必要單獨寫一個方法。
這才叫封裝嘛,你說是不是?

工廠方法也有點問題。主鍵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表:

複寫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

文章有點長了,下一篇我們繼續。