你們都知道軟件開發不是一蹴而就的事情,咱們不可能在不瞭解產品(或行業領域)的前提下進行軟件開發,在開發前一般須要進行大量的業務知識梳理,而後才能到軟件設計的層面,最後纔是開發。而在業務知識梳理的過程當中,必然會造成某個領域知識,根據領域知識來一步步驅動軟件設計,就是領域驅動設計(DDD,Domain-Driven Design)的基本概念 。java
在業務初期,功能大都很是簡單,普通的 CRUD 就基本能知足要求,此時系統是清晰的。但隨着產品的不斷迭代和演化,業務邏輯變得愈來愈複雜,咱們的系統也愈來愈冗雜。各個模塊之間彼此關聯,甚至到後期連相應的開發者都很難說清模塊的具體功能和意圖究竟是啥。這就會致使在想要修改一個功能時,要追溯到這個功能須要修改的點就要很長時間,更別提修改帶來的不可預知的影響面。 好比下圖所示:數據庫
訂單服務中提供了查詢、建立訂單相關的接口,也提供了訂單評價、支付的接口。同時訂單表是個大表,包含了很是多字段。咱們在維護代碼時,將會致使牽一髮而動全身,極可能本來咱們只是想改下評價相關的功能,卻影響到了建立訂單的核心流程。雖然咱們能夠經過測試來保證功能的完備性,但當咱們在訂單領域有大量需求同時並行開發時將會出現改動重疊、惡性循環、疲於奔命修改各類問題的局面,並且大量的全量回歸會給測試帶來不可接受的災難。設計模式
但現實中絕大部分公司都是這樣一個狀態,而後通常他們的解決方案是不斷的重構系統,讓系統的設計隨着業務成長也進行不斷的演進。經過重構出一些獨立的類來存放某些通用的邏輯解決混亂問題,可是咱們很難給它一個業務上的含義,只能以技術緯度進行描述,那麼帶來的問題就是其餘人接手這塊代碼的時候不知道這個的含義或者只能經過修改通用邏輯來達到某些需求。微信
實際上,領域模型自己也不是一個陌生的單詞,說直白點,在早期開發中,領域模型就是數據庫設計。由於你想:咱們作傳統項目的流程或者說包括如今咱們作項目的流程,都是首先討論需求,而後是數據庫建模,在需求逐步肯定的過程不斷的去變動數據庫的設計,接着咱們在項目開發階段,發現有些關係沒有建、有些字段少了、有些表結構設計不合理,又在不斷的去調整設計,最後上線。在傳統項目中,數據庫是整個項目的根本,數據模型出來之後後續的開發都是圍繞着數據展開,而後造成以下的一個架構 :數據結構
很顯然,這其中存在的問題以下:架構
Service層很重,全部邏輯處理基本都放在service層。dom
POJO做爲 Service 層很是重要的一個實體,會由於不一樣場景的需求作不一樣的變化和組合,就會形成 POJO 的幾種不一樣模型(失血、貧血、充血),以此用來形容領域模型太胖或者太瘦。數據庫設計
隨着業務變得複雜之後,包括數據結構的變化,那麼各個模塊就須要進行修改,本來清晰的系統通過不斷的演 化變得複雜、冗餘、耦合度高,後果就很是嚴重。函數
咱們試想一下若是一個軟件產品不依賴數據庫存儲設備,那咱們怎麼去設計這個軟件呢?若是沒有了數據存儲,那麼咱們的領域模型就得基於程序自己來設計。那這個就是 DDD 須要去考慮的問題。微服務
當一個對象由其標識(而不是屬性)區分時,這種對象稱爲實體(Entity)。好比當兩個對象的標識不一樣時,即便兩個對象的其餘屬性全都相同,咱們也認爲他們是兩個徹底不一樣的實體。
當一個對象用於對事物進行描述而沒有惟一標識時,那麼它被稱做值對象。由於在領域中並非任什麼時候候一個事物都須要有一個惟一的標識,也就是說咱們並不關心具體是哪一個事物,只關心這個事物是什麼。好比下單流程中,對於配送地址來講,只要是地址信息相同,咱們就認爲是同一個配送地址。因爲不具備惟一標示,咱們也不能說"這一個"值對象或者"那一個"值對象。
一些重要的領域行爲或操做,它們不太適合建模爲實體對象或者值對象,它們本質上只是一些操做,並非具體的事物,另外一方面這些操做每每又會涉及到多個領域對象的操做,它們只負責來協調這些領域對象完成操做而已,那麼咱們能夠歸類它們爲領域服務。它實現了所有業務邏輯而且經過各類校驗手段保證業務的正確性。同時呢,它也能避免在應用層出現領域邏輯。理解起來,領域服務有點facade的味道。
聚合是經過定義領域對象之間清晰的所屬關係以及邊界來實現領域模型的內聚,以此來避免造成錯綜複雜的、難以維護的對象關係網。聚合定義了一組具備內聚關係的相關領域對象的集合,咱們能夠把聚合看做是一個修改數據的單元。
聚合根屬於實體對象,它是領域對象中一個高度內聚的核心對象。(聚合根具備全局的惟一標識,而實體只有在聚合內部有惟一的本地標識,值對象沒有惟一標識,不存在這個值對象或那個值對象的說法)
若一個聚合僅有一個實體,那這個實體就是聚合根;但要有多個實體,咱們就要思考聚合內哪一個對象有獨立存在的意義且能夠和外部領域直接進行交互。
DDD中的工廠也是一種封裝思想的體現。引入工廠的緣由是:有時建立一個領域對象是一件相對比較複雜的事情,而不是簡單的new操做。工廠的做用是隱藏建立對象的細節。事實上大部分狀況下,領域對象的建立都不會相對太複雜,故咱們僅需使用簡單的構造函數建立對象就能夠。隱藏建立對象細節的好處是顯而易見的,這樣就能夠不會讓領域層的業務邏輯泄露到應用層,同時也減輕應用層負擔,它只要簡單調用領域工廠來建立出指望的對象就能夠了。
資源倉儲封裝了基礎設施來提供查詢和持久化聚合操做。這樣可以讓咱們始終關注在模型層面,把對象的存儲和訪問都委託給資源庫來完成。它不是數據庫的封裝,而是領域層與基礎設施之間的橋樑。DDD 關心的是領域內的模型,而不是數據庫的操做。
DDD 概念理解起來有點抽象, 這個有點像設計模式,感受頗有用,可是本身開發的時候又不知道怎麼應用到代碼裏面,或者生搬硬套後本身看起來都很彆扭,那麼接下來咱們就以一個簡單的轉盤抽獎案例來分析一下 DDD 的應用。
這個系統能夠劃分爲運營管理平臺和用戶使用層,運營平臺對於抽獎的配置比較複雜可是操做頻率會比較低。而用戶對抽獎活動頁面的使用是高頻率的可是對於配置規則來 說是誤感知的,根據這樣的特色,咱們把抽獎平臺劃分針 對 C 端抽獎和 M 端抽獎兩個子域。
在確認了 M 端領域和 C 端的限界上下文後,咱們再對各 自上下文內部進行限界上下文的劃分,接下來以 C 端用戶爲例來劃分界限上下文。
首先咱們要來了解該產品的基本需求
抽獎資格(什麼狀況下會有抽獎機會、抽獎次數、抽 獎的活動起始時間) 。
抽獎的獎品(實物、優惠券、理財金、購物卡…) 。
獎品自身的配置,機率、庫存、某些獎品在有限的機率下還只能被限制抽到多少次等。
風控對接, 防止惡意薅羊毛。
抽獎上下文是整個領域的核心,負責處理用戶抽獎的核心業務。
對於抽獎的限制,咱們定義了抽獎資格的通用語言,將抽獎開始 / 結束時間,抽獎可參與次數等限制條件都收攏到抽獎資格子域中。
因爲 C 端存在一些刷單行爲,咱們根據產品需求定義了風控上下文,用於對抽獎進行風控。
因爲抽獎和發放獎品其實能夠認爲是兩個領域,一個負責根據機率去抽獎、另外一個負責將抽中的獎品發放出去,因此對於這一塊也獨立出來一個領域。
經過上下文劃分之後,咱們還須要進一步梳理上下文之間的關係,梳理的好處在於:
任務更好拆分(一個開發人員能夠全身心投入到相關子域的上下文中) 。
方便溝通,明確自身上下文和其餘上下文之間的依賴關 系,能夠實現更好的對接。
在實際開發中,咱們通常會採用模塊來表示一個領域的界 限上下文,好比:
抽獎上下文:com.hafiz.business.lottery.*
風控上下文:com.hafiz.business.riskcontroller.*
獎品上下文:com.hafiz.business.prize.*
活動資格上下文 : com.hafiz.business.qualification.*
庫存上下文:com.hafiz.business.stock.*
對於模塊內的組織結構,通常狀況下咱們是按照領域對象、 領域服務、領域資源庫、防腐層等組織方式定義的。
領域對象-值對象: com.hafiz.business.lottery.domain.valobj.*
領域對象-實體: com.hafiz.business.lottery.domain.entity.*
領域對象-聚合根: com.hafiz.business.lottery.domain.aggregate.*
領域服務: com.hafiz.business.lottery.domain.service.*
領域資源庫: com.hafiz.business.lottery.domain.repo.*
領域防腐層:com.hafiz.business.lottery.domain.facade.*
部分代碼以下:
抽獎聚合根:
擁有抽獎活動id和該活動下全部可用的獎池列表,它最主要的領域功能是根據一個抽獎的場景(DrawLotteryContext),經過chooseAwardPool方法篩選出一個匹配的獎池。
package com.hafiz.business.lottery.domain.aggregate;
import ...;
public class DrawLottery {
private int lotteryId; // 抽獎id
private List<AwardPool> awardPools; // 獎池列表
public void setLotteryId(int lotteryId) {
if (lotteryId < 0) {
throw new IllegalArgumentException("非法的抽獎id");
}
this.lotteryId = lotteryId;
}
public AwardPool chooseAwardPool(DrawLotteryContext context) {
...
}
}
獎池值對象:
package com.hafiz.business.lottery.domain.valobj;
import ...;
public class AwardPool {
private String cityIds; // 獎池支持的城市
private String scores; // 獎池支持的得分
private int userGroupType; // 獎池匹配的用戶類型
private List<Award> awards; // 獎池中包含的獎品
public boolean matchedCity(int cityId) {
...
}
public boolean matchedScore(int score) {
...
}
public Award randomGetAward() {
int sumOfProbablity = 0;
for (Award award : awards) {
sumOfProbablity += award.getAwardProbablity();
}
int randomNumber = ThreadLocalRandom.current().netInt(sumOfProbablity);
int range = 0;
for (Award award : awards) {
range += award.getAwardProbablity();
if (randomNumber < range) {
return award;
}
}
return null;
}
}
抽獎資源庫:
咱們屏蔽對底層獎池及獎品的直接訪問,僅對抽獎的聚合根資源進行管理。
package com.hafiz.business.lottery.domain.repo;
import ...;
public class DrawLotteryRepository {
@Autowried
private AwardDao awardDao;
@Autowried
private AwardPoolDao awardPoolDao;
@Autowried
private DrawLotteryCacheAccessObj drawLotteryCacheAccessObj;
public DrawLottery getDrawLotteryById(int lotteryId) {
DrawLottery drawLottery = drawLotteryCacheAccessObj.get(lotteryId);
if (drawLottery != null) {
return drawLottery;
}
drawLottery = getDrawLotteryFromDB(lotteryId);
drawLotteryCacheAccessObj.add(lotteryId, drawLottery);
return drawLottery;
}
private DrawLottery getDrawLotteryFromDB() {
...
}
}
防腐層:
以用戶信息防腐層爲例,它的入參是抽獎請求參數(LotteryContext),輸出爲城市信息(CityInfo)。
package com.hafiz.business.lottery.domain.facade;
import ...;
public class UserCityInfoFacade {
@Autowried
private CityService cityService;
public CityInfo getCityInfo (LotteryContext context) {
CityRequest request = new CityRequest();
request.setLat(context.getLat());
request.setLng(context.getLng());
CityReponse reponse = cityService.getCityInfo(request);
return buildCityInfo(reponse);
}
private CityInfo buildCityInfo(CityReponse reponse) {
...
}
}
抽獎領域服務:
package com.hafiz.business.lottery.domain.service.impl;
import ...;
@Service
public class LotteryServiceImpl implements LotteryService {
@Autowried
private DrawLotteryRepository drawLotteryRepository;
@Autowried
private UserCityInfoFacade userCityInfoFacade;
@Autowried
private AwardSenderService awardSenderService;
@Autowried
private AwardCountFacade awardCountFacade;
public LotteryReponse drawLottery(LotteryContext context) {
// 獲取抽獎聚合根
DrawLottery drawLottery = drawLotteryRepository.getDrawLotteryById(context.getLotteryId());
// 增長抽獎計數信息
awardCountFacade.incrTryCount(context);
// 選中獎池
AwardPool awardPool = drawLottery.chooseAwardPool(context);
// 抽出獎品
Award award = awardPool.randomGetAward();
// 發出獎品
return buildLotteryReponse(awardSenderService.sendeAward(award, context));
}
private LotteryReponse buildLotteryReponse(AwardSendReponse awardSendReponse) {
...
}
}
用 DDD 能夠很好的解決領域模型到設計模型的同步、演進最後映射到實際的代碼邏輯,總的來講,DDD 開發模式有如下幾個好處 :
DDD 能讓咱們知道如何抽象出限界上下文以及如何去分而治之。
分而治之 : 把複雜的大規模軟件拆分紅若干個子模塊,每個模塊都能獨立運行和解決相關問題。而且分割後各個部分能夠組裝成爲一個總體。
抽象 : 使用抽象可以精簡問題空間,並且問題越小越容易理解,好比說咱們要對接支付,抽象的緯度應該是支付,而不是具體的微信支付仍是支付寶支付。
DDD 的限界上下文能夠完美匹配微服務的要求。在系統複雜以後,咱們都須要用分治來拆解問題。通常有兩種方式,技術維度和業務維度。技術維度是相似 MVC 這 樣,業務維度則是指按業務領域來劃分系統。 微服務架構更強調從業務維度去作分治來應對系統複雜度, 而 DDD 也是一樣的着重業務視角。
其實咱們能夠簡單認爲領域驅動設計是一種指導思想,是一種軟件開發方法。經過 DDD 咱們能夠將系統結構設計的更加合理, 以便最終知足高內聚低耦合的要求。在個人觀點來看,有點相似數據庫的三範式,咱們開始在學的時候並不太理解,當有足夠的設計經驗之後慢慢會體會到三範式帶來的好處。同時咱們也並不必定須要徹底嚴格按照這三範式去進行實踐,有些狀況下是能夠也須要靈活調整的。