筆者先前參與了一個有關汽車信息的網站開發,用於顯示不同品牌的汽車的信息,包括車型,發動機型號,車身尺寸和汽車報價等信息。在建模時,我們只需要創建名為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中。
對於以上建模方式,我至少可以看到兩處不足之處:
- 判斷某個Brand是否擁有某種Functionality更應該是Brand本身的一種行為,而不是通過Service來完成。
- 在有了新的需求之后,不同的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。