一次領域驅動設計(DDD)的實際應用


  筆者先前參與了一個有關汽車信息的網站開發,用於顯示不同品牌的汽車的信息,包括車型,發動機型號,車身尺寸和汽車報價等信息。在建模時,我們只需要創建名為Car的實體(Entity)對象。其他的信息,比如車身尺寸,都是對Car起描述作用的,因此應該建模成值對象(Value Object)。

 

  

  此時創建的Car對象如下:

public class Car { private String id; private CarType type; private EngineType engineType; private String brand; private double length; private double height; private double width; private int price; }

 

  對應的CarRepository為:

public interface CarRepository { List<Car> getAllCars(); Car getCarById(String id); }

 

  現在新的需求來了:對於有些品牌的汽車,該網站與這些品牌的汽車經銷商建立了合作關系,使得用戶在網站上點擊一個鏈接便可以進入對應的汽車經銷商網站。用戶每點擊一次鏈接,汽車經銷商都會給該網站相應的提成,這也成為了該網站的收入來源之一。該網站因此做出預測,在將來還會有更多這樣的定制化需求,即針對不同的品牌顯示不同的內容。

 

(一)錯誤的建模方法

  該網站的開發者立刻決定:可以將這些定制化需求建模成對象,名為Functionality,再在數據庫中存放這些Functionality和品牌(Brand)之間關聯關系。比如現在有兩種類型的定制化需求,一種即為上面講到的是否顯示經銷商鏈接,另一種即為是否顯示報價。因此,他們建立了以下數據庫表:

Functionality Brands
ShowAgencyLink BMW,HONDA
ShowPrice TOYOTA,VOLVO,HONDA

 

  相應地,他們創建了一個名為FuncitonalityEnablement的類與上表對應:

public class FunctionalityEnablement { private Functionality functionality; private String brands; }

 

  請注意,這里使用了一個String來包含多個Brand。要看某個品牌的汽車是否具有某個Functionality,可以通過以下Service類來完成:

public interface BrandFunctionalityService { boolean isFunctionalityEnabled(Functionality functionality, String brand); }

 

  該BrandFuntionalityService先通過DAO層獲取到某中Functionality在數據庫中所對應的FunctionalityEnablement,再調用isFunctionalityEnabled()方法,傳入Brand值,檢查該Brand是否擁有該Functionality,即檢查該Brand是否包含在FunctionalityEnablement中的brands中。

 

  對於以上建模方式,我至少可以看到兩處不足之處:

  1. 判斷某個Brand是否擁有某種Functionality更應該是Brand本身的一種行為,而不是通過Service來完成。
  2. 在有了新的需求之后,不同的Functionality對Brand起到了描述作用,並且這些描述信息有可能隨着時間改變,比如在之后某個時刻,該網站又與BUICK品牌的經銷商建立的合作關系。這樣一來,Brand不再是值對象了,而是變成了具有生命周期的實體對象。但是以上的解決方案依然將Brand作為值對象來使用,並且將本應該成為描述信息的Functionality當成了實體來使用,的確不應該。

 

(二)正確的建模方法——采用領域驅動設計(DDD)

  在使用領域驅動設計時,我們實際上可以建立兩個限界上下文(Bounded Context),一個為汽車目錄上下文(Car Category Context),另一個為品牌功能上下文(Brand Functionality Context)。在有些情況下,不同的上下文運行在不同的進程空間中,但是對於本文中的情況,由於兩個上下文聯系密切,又相對較小,我們可以通過引入不同的Java包來划分這兩個限界上下文。

 

  這樣一來,在汽車目錄上下文中,Brand依然可以建模成值對象,但是在品牌功能上下文中,Brand則應該建模成實體對象並且進行持久化。汽車目錄上下文將作為品牌功能上下文的下游,即依賴於品牌功能上下文。在汽車目錄上下文中,如果需要查看某個品牌是否擁有某種功能,我們可以調用品牌功能上下文所提供的應用服務(Application Service)。應用服務是非常薄的一層,限界上下文的領域模型便通過該層向外界提供基於用例的服務。

 

  這里我們將重點放在品牌功能上下文上。通過以上討論,我們知道,Brand應該為實體對象,並且擁有一種或多種Functionality,為了不至產生混淆,我們將實體類型的Brand命名為ConfigurableBrand。該ConfigurableBrand定義如下:

public class ConfigurableBrand { private String name; private List<Functionality> functionalities; public boolean hasFunctionality(Functionality functionality) { return functionalities.contains(functionality); } }

 

  對應的ConfigurableBrandRepository為:

public interface ConfigurableBrandRepository { public List<ConfigurableBrand> getAllConfigurableBrands(); public ConfigurableBrand getConfigurableBrandByName(String name); }

 

  在持久化ConfigurableBrand時,我們可以像上文中那樣,在不完全遵循關系型數據庫范式的情況下對其進行持久化,此時是將ConfigurableBrand的name作為主鍵,其他信息(這里只有Functionality)則序列化到一個列中:

BrandName Funcionalities
BMW ShowAgencyLink
TOYOTA ShowPrice
HONDA ShowAgencyLink,ShowPrice
VOLVO ShowPrice

 

  當然,如果你習慣了遵循數據庫范式,那么你也可以建立3張數據庫表,一張用於存放ConfigurableBrand,一張用於存放Functionality,另一張關聯表存放前兩者之間的關聯關系。此時,ConfigurableBrand和Functionality存在着多對多的關系。

 

  品牌功能上下文的應用服務提供了以下業務方法:

public interface ConfigurableBrandFunctionalityService { boolean isFunctionalityEnabled(String functionality, String brand); }

 

  當汽車目錄上下文需要知道某個品牌是否擁有某種功能時,它便應該調用品牌功能上下文的應用服務ConfigurableBrandFunctionalityService,該Service首先通過ConfigurableBrandRepository找到相應的ConfigurableBrand實體對象,再調用ConfigurableBrand中的hasFunctionality()方法以判斷該ConfigurableBrand是否擁有某種Functionality。

 

  對於ConfigurableBrandFunctionalityService,我們需要注意,首先外界上下文如果需要訪問品牌功能上下文,它必須通過ConfigurableBrandFunctionalityService應用服務,再由該應用服務委派給品牌功能的領域模型,即應用服務才是領域模型的直接客戶。另外,在調用ConfigurableBrandFunctionalityService時,我們並沒有傳入ConfigurableBrand和Functionality領域對象,而是直接使用了String類型,這也是合理的,因為外界不應該直接訪問品牌功能上下文中的領域模型,而是應該通過應用服務。再者,在上文中我們講到,isFunctionalityEnabled()方法更應該建模在ConfigurableBrand實體上,但是這里我們依然將其放在了ConfigurableBrandFunctionalityService上。原因在於,判斷一個品牌是否擁有某種功能的核心業務邏輯的確是放在ConfigurableBrand中的,即hasFunctionality()方法,而ConfigurableBrandFunctionalityService中的isFunctionalityEnabled()方法只是反應了一個業務用例,它本身並不處理業務邏輯,而是將邏輯委派給領域模型ConfigurableBrand。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM