長(zhǎng)文多圖:結(jié)合DDD講清楚編寫技術(shù)方案的七大維度
JAVA前線?
歡迎大家關(guān)注公眾號(hào)「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實(shí)際應(yīng)用、架構(gòu)思維、職場(chǎng)分享、產(chǎn)品思考等等,同時(shí)也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
1 為什么要寫技術(shù)方案
回顧軟件開發(fā)的歷史進(jìn)程,我們可以將其分為程序設(shè)計(jì)時(shí)代、程序系統(tǒng)時(shí)代和軟件工程時(shí)代三大歷史階段。
在程序設(shè)計(jì)時(shí)代(1946-1956),軟件開發(fā)主要依賴于個(gè)人編程技巧,技術(shù)文檔只要存在個(gè)人開發(fā)者的大腦即可,因?yàn)闆]有溝通和協(xié)作需要,編寫技術(shù)文檔也不具有緊迫性。
在程序系統(tǒng)時(shí)代(1956-1968),計(jì)算機(jī)性能顯著提升,應(yīng)用范圍和規(guī)模逐步擴(kuò)大,以至于依靠個(gè)人無(wú)法完成軟件的開發(fā),所以出現(xiàn)了團(tuán)隊(duì)合作。在早期團(tuán)隊(duì)合作過(guò)程中,開發(fā)者仍然保持了早期各自為戰(zhàn)的開發(fā)習(xí)慣,即使出現(xiàn)了一些方法論雛形,也無(wú)法從根本上控制溝通和協(xié)作的巨大成本,軟件危機(jī)就此出現(xiàn)。1968年國(guó)際學(xué)術(shù)會(huì)議提出了軟件危機(jī)和軟件工程的概念。
軟件危機(jī)的定義是落后的軟件生產(chǎn)方式無(wú)法滿足迅速增長(zhǎng)的計(jì)算機(jī)軟件需求,從而導(dǎo)致開發(fā)與維護(hù)過(guò)程中出現(xiàn)一系列嚴(yán)重問題的現(xiàn)象。軟件的工程定義是建立并使用完善的工程化原則,以較經(jīng)濟(jì)的手段獲得能在實(shí)際機(jī)器上有效運(yùn)行的可靠軟件的一系列方法
從此軟件開發(fā)進(jìn)入工程化階段,也應(yīng)運(yùn)而生了大量開發(fā)方法論和開發(fā)模型。其中標(biāo)準(zhǔn)和完善的文檔是軟件工程重要組成部分,可以很大程度上減少溝通和協(xié)作成本,而技術(shù)方案又是技術(shù)文檔重要組成部分。
2 技術(shù)方案要體現(xiàn)什么
軟件系統(tǒng)生命周期包括定義、開發(fā)、運(yùn)維、消亡這四大階段。定義階段包括定義問題、可行性研究和需求分析。開發(fā)階段包括概要設(shè)計(jì)、詳細(xì)設(shè)計(jì)、編碼和測(cè)試。運(yùn)維階段包括更正性維護(hù)、適應(yīng)性維護(hù)、預(yù)防性維護(hù)和完善性維護(hù)。消亡階段包括系統(tǒng)報(bào)廢和優(yōu)雅下線。

生命周期每個(gè)階段固然有各自的重要性,但是開發(fā)者更應(yīng)該關(guān)注定義階段與開發(fā)階段。定義階段需要解決為什么開發(fā)(why)、需求是什么(what)兩個(gè)問題,開發(fā)階段需要解決怎么設(shè)計(jì),怎么編碼,怎么測(cè)試(how)三個(gè)問題。
技術(shù)方案是否需要體現(xiàn)定義和開發(fā)的所有子階段?我認(rèn)為也無(wú)必要。問題定義和可行性研究主要由產(chǎn)品經(jīng)理負(fù)責(zé),測(cè)試階段主要由測(cè)試人員負(fù)責(zé),開發(fā)者可以關(guān)注但不是必須體現(xiàn)在技術(shù)方案。我認(rèn)為技術(shù)方案必須要體現(xiàn)需求分析、概要設(shè)計(jì)、詳細(xì)設(shè)計(jì)、編碼四個(gè)子階段。
3 七大維度
我認(rèn)為一份完整技術(shù)方案應(yīng)該至少具有七大維度,每個(gè)維度描述系統(tǒng)的一個(gè)側(cè)面,組合在一起最終描繪出整個(gè)系統(tǒng),這些維度分別是:
四色分領(lǐng)域
用例看功能
流程三劍客
領(lǐng)域與數(shù)據(jù)
縱橫做設(shè)計(jì)
分層看架構(gòu)
接口看對(duì)接
本文我們分析一個(gè)足球運(yùn)動(dòng)員信息管理系統(tǒng),這個(gè)系統(tǒng)我們可能也都沒有做過(guò),正好一起分析這個(gè)系統(tǒng)。需要說(shuō)明本文著重介紹方法論的落地,業(yè)務(wù)細(xì)節(jié)難以面面俱到。
3.1 四色分領(lǐng)域
3.1.1 流程梳理
首先梳理業(yè)務(wù)流程,這里有兩個(gè)問題需要考慮,第一個(gè)問題是從什么視角去梳理?因?yàn)椴煌娜丝吹降牧鞒淌遣灰粯拥?。答案是取決于系統(tǒng)需要解決什么問題,因?yàn)槲覀円芾磉\(yùn)動(dòng)員從轉(zhuǎn)會(huì)到上場(chǎng)比賽整條鏈路信息,所以從運(yùn)動(dòng)員視角出發(fā)是一個(gè)合適的選擇。
第二個(gè)問題是對(duì)業(yè)務(wù)不熟悉怎么辦?因?yàn)槲覀儾皇求w育和運(yùn)動(dòng)專家,并不清楚整條鏈路的業(yè)務(wù)細(xì)節(jié)。答案是梳理流程時(shí)一定要有業(yè)務(wù)專家在場(chǎng),因?yàn)闆]有真實(shí)業(yè)務(wù)細(xì)節(jié),無(wú)法領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)。同理在互聯(lián)網(wǎng)梳理復(fù)雜業(yè)務(wù)流程時(shí),一定要有對(duì)相關(guān)業(yè)務(wù)熟悉的產(chǎn)品經(jīng)理或者運(yùn)營(yíng)一起參與。
假設(shè)足球業(yè)務(wù)專家梳理出了業(yè)務(wù)流程,運(yùn)動(dòng)員提出轉(zhuǎn)會(huì),協(xié)商一致后到新俱樂部體檢,體檢通過(guò)就進(jìn)行簽約。進(jìn)入新俱樂部后進(jìn)行訓(xùn)練,訓(xùn)練指標(biāo)達(dá)標(biāo)后上場(chǎng)比賽,賽后參加新聞發(fā)布會(huì)。實(shí)際流程會(huì)復(fù)雜很多,本文還是著重講解方法論。

3.1.2 四色建模
(1) 時(shí)標(biāo)對(duì)象
四色建模第一種顏色是紅色,表示時(shí)標(biāo)對(duì)象。時(shí)標(biāo)對(duì)象是四色建模最重要的對(duì)象,可以理解為核心業(yè)務(wù)單據(jù)。在業(yè)務(wù)過(guò)程中一定要對(duì)關(guān)鍵業(yè)務(wù)留下單據(jù),通過(guò)這些單據(jù)可以追溯整個(gè)業(yè)務(wù)流程。
時(shí)標(biāo)對(duì)象具有兩個(gè)特點(diǎn):第一是事實(shí)不可變性,記錄了過(guò)去某個(gè)時(shí)間點(diǎn)或時(shí)間段內(nèi)發(fā)生的事實(shí)。第二是責(zé)任可追溯性,記錄了管理者關(guān)注的信息?,F(xiàn)在我們分析本系統(tǒng)時(shí)標(biāo)對(duì)象有哪些,需要留下哪些核心業(yè)務(wù)單據(jù)。
轉(zhuǎn)會(huì)對(duì)應(yīng)轉(zhuǎn)會(huì)單據(jù),體檢對(duì)應(yīng)體檢單據(jù),簽合同對(duì)應(yīng)合同單據(jù),訓(xùn)練對(duì)應(yīng)訓(xùn)練指標(biāo)單據(jù),比賽對(duì)應(yīng)比賽指標(biāo)單據(jù),新聞發(fā)布會(huì)對(duì)應(yīng)采訪單據(jù)。根據(jù)分析繪制如下時(shí)標(biāo)對(duì)象:

(2) 參與方、地、物
這三類對(duì)象在四色建模中用綠色表示,我們以電商場(chǎng)景為例進(jìn)行說(shuō)明。用戶支付購(gòu)買商家的商品時(shí),用戶和商家是參與方。物流系統(tǒng)發(fā)貨時(shí)配送單據(jù)需要有配送地址對(duì)象,地址對(duì)象就是地。訂單需要商品對(duì)象,物流配送需要有貨品,商品和貨品就是物。
我們分析本例可以知道參與方包含總經(jīng)理、隊(duì)醫(yī)、教練、球迷、記者,地包含訓(xùn)練地址、比賽地址、采訪地址,物包含簽名球衣和簽名足球:

(3) 角色對(duì)象
在四色建模中用黃色表示,這類對(duì)象表示參與方、地、物以什么角色參與到業(yè)務(wù)流程:

(4) 描述對(duì)象
我們可以為對(duì)象增加相關(guān)描述信息,在四色建模中用藍(lán)色表示:

3.1.3 劃分領(lǐng)域
在四色建模過(guò)程中我們體會(huì)到時(shí)標(biāo)對(duì)象是最重要的對(duì)象,因?yàn)槠涑休d了業(yè)務(wù)系統(tǒng)核心單據(jù)。在劃分領(lǐng)域時(shí)我們同樣離不開時(shí)標(biāo)對(duì)象,通過(guò)收斂相關(guān)時(shí)標(biāo)對(duì)象劃分領(lǐng)域。

3.1.4 領(lǐng)域事件
當(dāng)業(yè)務(wù)系統(tǒng)發(fā)生一件事情時(shí),如果本領(lǐng)域或其它領(lǐng)域有后續(xù)動(dòng)作跟進(jìn),那么我們把這件事情稱為領(lǐng)域事件,這個(gè)事件需要被感知。
例如球員比賽受傷,這是比賽子域事件,但是醫(yī)療和訓(xùn)練子域是需要感知的,那么比賽子域就發(fā)出一個(gè)事件,醫(yī)療和訓(xùn)練子域會(huì)訂閱。球員比賽取得進(jìn)球,這也是比賽子域事件,但是訓(xùn)練和合同子域也會(huì)關(guān)注這個(gè)事件,所以比賽子域也會(huì)發(fā)出一個(gè)比賽進(jìn)球事件,訓(xùn)練和合同子域會(huì)訂閱。
通過(guò)事件交互有一個(gè)問題需要注意,通過(guò)事件訂閱實(shí)現(xiàn)業(yè)務(wù)只能采用最終一致性,需要放棄強(qiáng)一致性,可能會(huì)引入新的復(fù)雜度需要權(quán)衡。

3.2 用例看功能
目前為止領(lǐng)域已經(jīng)確定了,大領(lǐng)域已經(jīng)拆分成了小領(lǐng)域,我們已經(jīng)不再束手無(wú)策,而是可以對(duì)小領(lǐng)域進(jìn)行用例分析了。用例圖由參與者和用例組成,目的是回答這樣一個(gè)問題:什么人使用系統(tǒng)干什么事。
下圖表示在比賽領(lǐng)域,運(yùn)動(dòng)員視角(什么人)使用系統(tǒng)進(jìn)行進(jìn)球統(tǒng)計(jì),助攻統(tǒng)計(jì),犯規(guī)統(tǒng)計(jì),跑動(dòng)距離統(tǒng)計(jì),比賽評(píng)分統(tǒng)計(jì),傳球成功率統(tǒng)計(jì),受傷統(tǒng)計(jì)(干什么事),同理也可以選擇四色建模中其它參與者視角繪制用例圖。

include關(guān)鍵字表示包含關(guān)系。例如比賽是基用例,包含了進(jìn)球統(tǒng)計(jì),助攻統(tǒng)計(jì),犯規(guī)統(tǒng)計(jì),跑動(dòng)距離統(tǒng)計(jì),比賽評(píng)分統(tǒng)計(jì),傳球成功率統(tǒng)計(jì),受傷統(tǒng)計(jì)七個(gè)子用例。包含關(guān)系表示法有兩個(gè)優(yōu)點(diǎn):第一是可以清晰地組織子用例,第二是有利于子用例復(fù)用,例如主教練視角用例圖也包含比賽評(píng)分,那么就可以直接指向比賽評(píng)分子用例。
extend關(guān)鍵字表示擴(kuò)展關(guān)系。例如點(diǎn)球統(tǒng)計(jì)是進(jìn)球統(tǒng)計(jì)的擴(kuò)展,因?yàn)椴灰欢梢垣@得點(diǎn)球,所以點(diǎn)球統(tǒng)計(jì)即使不存在,也不會(huì)影響進(jìn)球統(tǒng)計(jì)功能。黃牌統(tǒng)計(jì)、紅牌統(tǒng)計(jì)是犯規(guī)統(tǒng)計(jì)的擴(kuò)展,因?yàn)槠胀ǚ敢?guī)不會(huì)獲得紅黃牌,所以紅黃牌統(tǒng)計(jì)不存在,也不會(huì)影響犯規(guī)統(tǒng)計(jì)功能。
用例圖不關(guān)心實(shí)現(xiàn)細(xì)節(jié),而是從外部視角描述系統(tǒng)功能,即使不了解實(shí)現(xiàn)細(xì)節(jié)的人,通過(guò)看用例圖也可以快速了解系統(tǒng)功能,這個(gè)特性規(guī)定了用例圖不宜過(guò)于復(fù)雜,能夠說(shuō)明核心功能即可。
3.3 流程三劍客
用例圖是從外部視角描述系統(tǒng),但是分析系統(tǒng)總是要深入系統(tǒng)內(nèi)部的,其中流程視圖就是描述系統(tǒng)內(nèi)如何流轉(zhuǎn)的視圖。
活動(dòng)圖、序列圖、狀態(tài)機(jī)圖是流程視圖中最重要的三種視圖,我們稱為流程三劍客。三者側(cè)重點(diǎn)有所不同:活動(dòng)圖側(cè)重于邏輯分支,順序圖側(cè)重于交互,狀態(tài)機(jī)圖側(cè)重于狀態(tài)流轉(zhuǎn)。
3.3.1 活動(dòng)圖
活動(dòng)圖適合描述復(fù)雜邏輯分支,設(shè)想這樣一種業(yè)務(wù)場(chǎng)景,球隊(duì)需要選拔一名球員成為足球先生,選拔標(biāo)準(zhǔn)如下:前場(chǎng)、中場(chǎng)、后場(chǎng)、門將各選出一名候選球員。前場(chǎng)隊(duì)員依次比較進(jìn)球數(shù)、助攻數(shù),中場(chǎng)隊(duì)員依次比較助攻數(shù)、搶斷數(shù),后場(chǎng)隊(duì)員依次比較解圍數(shù)、搶斷數(shù),門將依次比較撲救數(shù)、撲點(diǎn)數(shù),如果所有指標(biāo)均相同則抽簽。每個(gè)位置有人選之后,全體教練組投票,如果投票數(shù)相同則抽簽。
業(yè)界流傳著一句話:一圖勝千言,其中一個(gè)重要原因是文字是線性的,所以表達(dá)邏輯分支能力不如流程視圖,而在流程視圖中表達(dá)邏輯分支能力最強(qiáng)正是活動(dòng)圖。

3.3.2 順序圖
順序圖側(cè)重于交互,適合按照時(shí)間順序體現(xiàn)一個(gè)業(yè)務(wù)流程中交互細(xì)節(jié),但是順序圖并不擅長(zhǎng)體現(xiàn)復(fù)雜邏輯分支。
如果某個(gè)邏輯分支特別重要,可以選擇再畫一個(gè)順序圖。例如支付流程中有支付成功正常流程,也有支付失敗異常流程,這兩個(gè)流程都非常重要,所以可以用兩張順序圖體現(xiàn)?;氐奖疚膶?shí)例,我們可以通過(guò)順序圖體現(xiàn)球員從提出轉(zhuǎn)會(huì)到比賽全流程。

3.3.3 狀態(tài)機(jī)圖
假設(shè)一條數(shù)據(jù)有ABC三種狀態(tài),從正常業(yè)務(wù)角度來(lái)看,狀態(tài)只能從A流轉(zhuǎn)到B,再?gòu)腂流轉(zhuǎn)到C,不能亂序也不可逆。但是可能出現(xiàn)這種異常情況:數(shù)據(jù)當(dāng)前狀態(tài)為A,接收異步消息更改狀態(tài),B消息由于延時(shí)晚于C消息,最終導(dǎo)致狀態(tài)先改為C再改為B,那么此時(shí)狀態(tài)就是錯(cuò)誤的。
狀態(tài)機(jī)圖側(cè)重于狀態(tài)流轉(zhuǎn),說(shuō)明了哪些狀態(tài)之間可以相互流轉(zhuǎn),在實(shí)際開發(fā)中再結(jié)合狀態(tài)機(jī)代碼模式,可以解決上述狀態(tài)異常情況。回到本文實(shí)例,我們可以通過(guò)狀態(tài)機(jī)圖表示球員從提出轉(zhuǎn)會(huì)到簽約整個(gè)狀態(tài)流程。

3.4 領(lǐng)域與數(shù)據(jù)
上述章節(jié)從功能層面和流程層面進(jìn)行了系統(tǒng)分析,現(xiàn)在從數(shù)據(jù)層分析系統(tǒng),我們首先對(duì)比兩組概念:值對(duì)象與實(shí)體,領(lǐng)域?qū)ο笈c數(shù)據(jù)對(duì)象。
實(shí)體是具有唯一標(biāo)識(shí)的對(duì)象,唯一標(biāo)識(shí)會(huì)伴隨實(shí)體對(duì)象整個(gè)生命周期并且不可變更。值對(duì)象本質(zhì)上是屬性的集合,沒有唯一標(biāo)識(shí)。
領(lǐng)域?qū)ο笈c數(shù)據(jù)對(duì)象一個(gè)重要的區(qū)別是值對(duì)象存儲(chǔ)方式。領(lǐng)域?qū)ο笤诎祵?duì)象的同時(shí)也保留了值對(duì)象的業(yè)務(wù)含義,而數(shù)據(jù)對(duì)象可以使用更加松散的結(jié)構(gòu)保存值對(duì)象,簡(jiǎn)化數(shù)據(jù)庫(kù)設(shè)計(jì)。
現(xiàn)在我們需要管理足球運(yùn)動(dòng)員基本信息和比賽數(shù)據(jù),對(duì)應(yīng)領(lǐng)域模型和數(shù)據(jù)模型應(yīng)該如何設(shè)計(jì)?姓名、身高、體重是一名運(yùn)動(dòng)員本質(zhì)屬性,加上唯一編號(hào)可以對(duì)應(yīng)實(shí)體對(duì)象。跑動(dòng)距離,傳球成功率,進(jìn)球數(shù)是運(yùn)動(dòng)員比賽表現(xiàn),這些屬性的集合可以對(duì)應(yīng)值對(duì)象。

我們根據(jù)圖示編寫領(lǐng)域?qū)ο笈c數(shù)據(jù)對(duì)象代碼:
//?數(shù)據(jù)對(duì)象
public?class?FootballPlayerDO?{
????private?Long?id;
????private?String?name;
????private?Integer?height;
????private?Integer?weight;
????private?String?gamePerformance;
}
//?領(lǐng)域?qū)ο?/span>
public?class?FootballPlayerDMO?{
????private?Long?id;
????private?String?name;
????private?Integer?height;
????private?Integer?weight;
????private?GamePerformanceVO?gamePerformanceVO;
}
public?class?GamePerformanceVO?{
????private?Double?runDistance;
????private?Double?passSuccess;
????private?Integer?scoreNum;
}
為什么要采用JSON存儲(chǔ)值對(duì)象?因?yàn)槟_本化是一種拓展靈活性的方式,腳本化不僅指使用groovy、QLExpress腳本增強(qiáng)系統(tǒng)靈活性,還包括松散可擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)。數(shù)據(jù)模型抽象出了姓名、身高、體重這些基本屬性,對(duì)于頻繁變化的比賽表現(xiàn)屬性,這些屬性值可能經(jīng)常變化,甚至屬性本身也是經(jīng)常變化,可能會(huì)加上射門次數(shù),突破次數(shù)等,所以采用松散結(jié)構(gòu)進(jìn)行存儲(chǔ)。
如果需要根據(jù)JSON結(jié)構(gòu)中KEY進(jìn)行檢索,例如查詢進(jìn)球數(shù)大于5的球員,這也不是沒有辦法。我們可以將MySQL表中數(shù)據(jù)平鋪到ES中,一條數(shù)據(jù)根據(jù)JSON KEY平鋪?zhàn)兂蔀槎鄺l數(shù)據(jù),這樣就可以進(jìn)行檢索了。
3.5 縱橫做設(shè)計(jì)
復(fù)雜業(yè)務(wù)之所以復(fù)雜,一個(gè)重要原因是涉及角色或者類型較多,很難平鋪直敘地進(jìn)行設(shè)計(jì),所以我們需要增加分析維度。其中最常見的是增加橫向和縱向兩個(gè)維度,本文也著重討論兩個(gè)維度??傮w而言橫向擴(kuò)展的是思考廣度,縱向擴(kuò)展的是思考深度,對(duì)應(yīng)到系統(tǒng)設(shè)計(jì)而言可以總結(jié)為:縱向做隔離,橫向做編排。
我們首先分析一個(gè)下單場(chǎng)景。當(dāng)前有ABC三種訂單類型:A訂單價(jià)格9折,物流最大重量不能超過(guò)9公斤,不支持退款。B訂單價(jià)格8折,物流最大重量不能超過(guò)8公斤,支持退款。C訂單價(jià)格7折,物流最大重量不能超過(guò)7公斤,支持退款。按照需求字面含義平鋪直敘地寫代碼也并不難:
public?class?OrderServiceImpl?implements?OrderService?{
????@Resource
????private?OrderMapper?orderMapper;
????@Override
????public?void?createOrder(OrderBO?orderBO)?{
????????if?(null?==?orderBO)?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????if?(OrderTypeEnum.isNotValid(orderBO.getType()))?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????//?A類型訂單
????????if?(OrderTypeEnum.A_TYPE.getCode().equals(orderBO.getType()))?{
????????????orderBO.setPrice(orderBO.getPrice()?*?0.9);
????????????if?(orderBO.getWeight()?>?9)?{
????????????????throw?new?RuntimeException("超過(guò)物流最大重量");
????????????}
????????????orderBO.setRefundSupport(Boolean.FALSE);
????????}
????????//?B類型訂單
????????else?if?(OrderTypeEnum.B_TYPE.getCode().equals(orderBO.getType()))?{
????????????orderBO.setPrice(orderBO.getPrice()?*?0.8);
????????????if?(orderBO.getWeight()?>?8)?{
????????????????throw?new?RuntimeException("超過(guò)物流最大重量");
????????????}
????????????orderBO.setRefundSupport(Boolean.TRUE);
????????}
????????//?C類型訂單
????????else?if?(OrderTypeEnum.C_TYPE.getCode().equals(orderBO.getType()))?{
????????????orderBO.setPrice(orderBO.getPrice()?*?0.7);
????????????if?(orderBO.getWeight()?>?7)?{
????????????????throw?new?RuntimeException("超過(guò)物流最大重量");
????????????}
????????????orderBO.setRefundSupport(Boolean.TRUE);
????????}
????????//?保存數(shù)據(jù)
????????OrderDO?orderDO?=?new?OrderDO();
????????BeanUtils.copyProperties(orderBO,?orderDO);
????????orderMapper.insert(orderDO);
????}
}
上述代碼從功能上完全可以實(shí)現(xiàn)業(yè)務(wù)需求,但是程序員不僅要滿足功能,還需要思考代碼的可維護(hù)性。如果新增一種訂單類型,或者新增一個(gè)訂單屬性處理邏輯,那么我們就要在上述邏輯中新增代碼,如果處理不慎就會(huì)影響原有邏輯。
為了避免牽一發(fā)而動(dòng)全身這種情況,設(shè)計(jì)模式中的開閉原則要求我們面向新增開放,面向修改關(guān)閉,我認(rèn)為這是設(shè)計(jì)模式中最重要的一條原則。
需求變化通過(guò)擴(kuò)展,而不是通過(guò)修改已有代碼實(shí)現(xiàn),這樣就保證代碼穩(wěn)定性。擴(kuò)展也不是隨意擴(kuò)展,因?yàn)槭孪榷x了算法,擴(kuò)展也是根據(jù)算法擴(kuò)展,用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。標(biāo)準(zhǔn)意義的二十三種設(shè)計(jì)模式說(shuō)到底最終都是在遵循開閉原則。
如何改變平鋪直敘的思考方式?這就要為問題分析加上縱向和橫向兩個(gè)維度,我選擇使用分析矩陣方法,其中縱向表示策略,橫向表示場(chǎng)景:

3.5.1 縱向做隔離
縱向維度表示策略,不同策略在邏輯上和業(yè)務(wù)上應(yīng)該是隔離的,本實(shí)例包括優(yōu)惠策略、物流策略和退款策略,策略作為抽象,不同訂單類型去擴(kuò)展這個(gè)抽象,策略模式非常適合這種場(chǎng)景。本文詳細(xì)分析優(yōu)惠策略,物流策略和退款策略同理。
//?優(yōu)惠策略
public?interface?DiscountStrategy?{
????public?void?discount(OrderBO?orderBO);
}
//?A類型優(yōu)惠策略
@Component
public?class?TypeADiscountStrategy?implements?DiscountStrategy?{
????@Override
????public?void?discount(OrderBO?orderBO)?{
????????orderBO.setPrice(orderBO.getPrice()?*?0.9);
????}
}
//?B類型優(yōu)惠策略
@Component
public?class?TypeBDiscountStrategy?implements?DiscountStrategy?{
????@Override
????public?void?discount(OrderBO?orderBO)?{
????????orderBO.setPrice(orderBO.getPrice()?*?0.8);
????}
}
//?C類型優(yōu)惠策略
@Component
public?class?TypeCDiscountStrategy?implements?DiscountStrategy?{
????@Override
????public?void?discount(OrderBO?orderBO)?{
????????orderBO.setPrice(orderBO.getPrice()?*?0.7);
????}
}
//?優(yōu)惠策略工廠
@Component
public?class?DiscountStrategyFactory?implements?InitializingBean?{
????private?Map?strategyMap?=?new?HashMap<>();
????@Resource
????private?TypeADiscountStrategy?typeADiscountStrategy;
????@Resource
????private?TypeBDiscountStrategy?typeBDiscountStrategy;
????@Resource
????private?TypeCDiscountStrategy?typeCDiscountStrategy;
????public?DiscountStrategy?getStrategy(String?type)?{
????????return?strategyMap.get(type);
????}
????@Override
????public?void?afterPropertiesSet()?throws?Exception?{
????????strategyMap.put(OrderTypeEnum.A_TYPE.getCode(),?typeADiscountStrategy);
????????strategyMap.put(OrderTypeEnum.B_TYPE.getCode(),?typeBDiscountStrategy);
????????strategyMap.put(OrderTypeEnum.C_TYPE.getCode(),?typeCDiscountStrategy);
????}
}
//?優(yōu)惠策略執(zhí)行
@Component
public?class?DiscountStrategyExecutor?{
????private?DiscountStrategyFactory?discountStrategyFactory;
????public?void?discount(OrderBO?orderBO)?{
????????DiscountStrategy?discountStrategy?=?discountStrategyFactory.getStrategy(orderBO.getType());
????????if?(null?==?discountStrategy)?{
????????????throw?new?RuntimeException("無(wú)優(yōu)惠策略");
????????}
????????discountStrategy.discount(orderBO);
????}
}
3.5.2 橫向做編排
橫向維度表示場(chǎng)景,一種訂單類型在廣義上可以認(rèn)為是一種業(yè)務(wù)場(chǎng)景,在場(chǎng)景中將獨(dú)立的策略進(jìn)行串聯(lián),模板方法設(shè)計(jì)模式適用于這種場(chǎng)景。
模板方法模式一般使用抽象類定義算法骨架,同時(shí)定義一些抽象方法,這些抽象方法延遲到子類實(shí)現(xiàn),這樣子類不僅遵守了算法骨架約定,也實(shí)現(xiàn)了自己的算法。既保證了規(guī)約也兼顧靈活性,這就是用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。
//?創(chuàng)建訂單服務(wù)
public?interface?CreateOrderService?{
????public?void?createOrder(OrderBO?orderBO);
}
//?抽象創(chuàng)建訂單流程
public?abstract?class?AbstractCreateOrderFlow?{
????@Resource
????private?OrderMapper?orderMapper;
????public?void?createOrder(OrderBO?orderBO)?{
????????//?參數(shù)校驗(yàn)
????????if?(null?==?orderBO)?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????if?(OrderTypeEnum.isNotValid(orderBO.getType()))?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????//?計(jì)算優(yōu)惠
????????discount(orderBO);
????????//?計(jì)算重量
????????weighing(orderBO);
????????//?退款支持
????????supportRefund(orderBO);
????????//?保存數(shù)據(jù)
????????OrderDO?orderDO?=?new?OrderDO();
????????BeanUtils.copyProperties(orderBO,?orderDO);
????????orderMapper.insert(orderDO);
????}
????public?abstract?void?discount(OrderBO?orderBO);
????public?abstract?void?weighing(OrderBO?orderBO);
????public?abstract?void?supportRefund(OrderBO?orderBO);
}
//?實(shí)現(xiàn)創(chuàng)建訂單流程
@Service
public?class?CreateOrderFlow?extends?AbstractCreateOrderFlow?{
????@Resource
????private?DiscountStrategyExecutor?discountStrategyExecutor;
????@Resource
????private?ExpressStrategyExecutor?expressStrategyExecutor;
????@Resource
????private?RefundStrategyExecutor?refundStrategyExecutor;
????@Override
????public?void?discount(OrderBO?orderBO)?{
????????discountStrategyExecutor.discount(orderBO);
????}
????@Override
????public?void?weighing(OrderBO?orderBO)?{
????????expressStrategyExecutor.weighing(orderBO);
????}
????@Override
????public?void?supportRefund(OrderBO?orderBO)?{
????????refundStrategyExecutor.supportRefund(orderBO);
????}
}
3.5.3 綜合應(yīng)用
上述實(shí)例業(yè)務(wù)和代碼并不復(fù)雜,其實(shí)復(fù)雜業(yè)務(wù)場(chǎng)景也不過(guò)是簡(jiǎn)單場(chǎng)景的疊加、組合和交織,無(wú)外乎也是通過(guò)縱向做隔離、橫向做編排尋求答案。

縱向維度抽象出能力池這個(gè)概念,能力池中包含許多能力,不同的能力按照不同業(yè)務(wù)維度聚合,例如優(yōu)惠能力池,物流能力池,退款能力池。我們可以看到兩種程度的隔離性,能力池之間相互隔離,能力之間也相互隔離。
橫向維度將能力從能力池選出來(lái),按照業(yè)務(wù)需求串聯(lián)在一起,形成不同業(yè)務(wù)流程。因?yàn)槟芰梢匀我饨M合,所以體現(xiàn)了很強(qiáng)的靈活性。除此之外,不同能力既可以串行執(zhí)行,如果不同能力之間沒有依賴關(guān)系,也可以如同流程Y一樣并行執(zhí)行,提升執(zhí)行效率。
此時(shí)我們回到本文足球運(yùn)動(dòng)員管理系統(tǒng),如果采用縱向和橫向思維分析3.3.1足球先生選拔業(yè)務(wù)場(chǎng)景可以得到下圖:

縱向隔離出進(jìn)攻能力池,防守能力池,門將能力池,橫向編排出前場(chǎng)、中場(chǎng)、后場(chǎng)、門將四個(gè)流程,在不同流程中可以任意從能力池中選擇能力進(jìn)行組合,而不是編寫冗長(zhǎng)的判斷邏輯,顯著提升了代碼可擴(kuò)展性。
3.6 分層看架構(gòu)
系統(tǒng)架構(gòu)總體而言分為兩個(gè)層次,第一種層次是指本項(xiàng)目在整個(gè)公司位于哪一層次。持久層、緩存層、中間件、業(yè)務(wù)中臺(tái)、服務(wù)層、網(wǎng)關(guān)層、客戶端和代理層是常見的分層架構(gòu),大多數(shù)情況下業(yè)務(wù)需求最終會(huì)體現(xiàn)在服務(wù)層,不同的業(yè)務(wù)領(lǐng)域?qū)?yīng)不同的微服務(wù)。

第二種層次是指本項(xiàng)目?jī)?nèi)部代碼的組織方式,一般可以分為接口層,訪問層,業(yè)務(wù)層,領(lǐng)域?qū)?,外部訪問層和基礎(chǔ)層。
(1) api
接口層:提供面向外部接口聲明和DTO
(2) controller
訪問層:提供HTTP訪問入口
(3) service
業(yè)務(wù)層:提供BO對(duì)象,領(lǐng)域?qū)雍蜆I(yè)務(wù)層都包含業(yè)務(wù),但是用途不同。業(yè)務(wù)層可以組合不同領(lǐng)域業(yè)務(wù),并且可以增加流控、監(jiān)控、日志、權(quán)限控制切面,相較于領(lǐng)域?qū)痈鼮樨S富
(4) domain
領(lǐng)域?qū)樱禾峁〥MO、VO、事件、DO和數(shù)據(jù)訪問,核心是根據(jù)領(lǐng)域進(jìn)行分包,領(lǐng)域內(nèi)高內(nèi)聚,領(lǐng)域間低耦合
(5) dependency
外部訪問層:在這個(gè)模塊中調(diào)用外部RPC服務(wù),解析返回碼和返回?cái)?shù)據(jù)
(6) infrastructure
基礎(chǔ)層:包含通用基礎(chǔ)功能,例如基礎(chǔ)工具,緩存工具,打印日志,消息發(fā)送

本文僅展開領(lǐng)域?qū)舆M(jìn)行分析。領(lǐng)域?qū)雍诵氖前凑疹I(lǐng)域進(jìn)行分包,并且提供DMO、VO、事件、DO和數(shù)據(jù)訪問,領(lǐng)域內(nèi)高內(nèi)聚,領(lǐng)域間低耦合。

3.7 接口看對(duì)接
一個(gè)接口代碼編寫完成后,那么這個(gè)接口如何調(diào)用,輸入和輸出參數(shù)是什么,這些問題需要在接口文檔中得到回答。
接口文檔生成有兩種方式,第一種方式是自動(dòng)生成,例如使用Swagger框架,第二種方式是手工生成。自動(dòng)生成的優(yōu)點(diǎn)是代碼即文檔,還具有調(diào)試功能,在公司內(nèi)部進(jìn)行聯(lián)調(diào)時(shí)非常方便。但是如果接口是提供給外部第三方使用,那么還是需要手工編寫接口文檔。一個(gè)接口核心描述無(wú)外乎接口名稱、接口說(shuō)明、輸入?yún)?shù)、輸出參數(shù),其它信息根據(jù)需要再增加。

4 文章總結(jié)
本文通過(guò)一個(gè)業(yè)務(wù)實(shí)例介紹了技術(shù)方案的七大維度:四色分領(lǐng)域、用例看功能、流程三劍客、領(lǐng)域與數(shù)據(jù)、縱橫做設(shè)計(jì)、分層看架構(gòu)、接口看對(duì)接。每個(gè)維度描述系統(tǒng)的一個(gè)側(cè)面,組合在一起最終描繪出整個(gè)系統(tǒng)。
在實(shí)際開發(fā)中如果需求不復(fù)雜,那么也不是七個(gè)維度都要體現(xiàn),而是根據(jù)實(shí)際情況取舍,能夠把方案說(shuō)清楚即可,希望本文對(duì)大家有所幫助。
JAVA前線?
歡迎大家關(guān)注公眾號(hào)「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實(shí)際應(yīng)用、架構(gòu)思維、職場(chǎng)分享、產(chǎn)品思考等等,同時(shí)也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
