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è)人無法完成軟件的開發(fā),所以出現(xiàn)了團(tuán)隊(duì)合作。在早期團(tuán)隊(duì)合作過程中,開發(fā)者仍然保持了早期各自為戰(zhàn)的開發(fā)習(xí)慣,即使出現(xiàn)了一些方法論雛形,也無法從根本上控制溝通和協(xié)作的巨大成本,軟件危機(jī)就此出現(xiàn)。1968年國際學(xué)術(shù)會議提出了軟件危機(jī)和軟件工程的概念。
軟件危機(jī)的定義是落后的軟件生產(chǎn)方式無法滿足迅速增長的計(jì)算機(jī)軟件需求,從而導(dǎo)致開發(fā)與維護(hù)過程中出現(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ì)、編碼和測試。運(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ì),怎么編碼,怎么測試(how)三個(gè)問題。
技術(shù)方案是否需要體現(xiàn)定義和開發(fā)的所有子階段?我認(rèn)為也無必要。問題定義和可行性研究主要由產(chǎn)品經(jīng)理負(fù)責(zé),測試階段主要由測試人員負(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)
接口看對接
本文我們分析一個(gè)足球運(yùn)動員信息管理系統(tǒng),這個(gè)系統(tǒng)我們可能也都沒有做過,正好一起分析這個(gè)系統(tǒng)。需要說明本文著重介紹方法論的落地,業(yè)務(wù)細(xì)節(jié)難以面面俱到。
3.1 四色分領(lǐng)域
3.1.1 流程梳理
首先梳理業(yè)務(wù)流程,這里有兩個(gè)問題需要考慮,第一個(gè)問題是從什么視角去梳理?因?yàn)椴煌娜丝吹降牧鞒淌遣灰粯拥摹4鸢甘侨Q于系統(tǒng)需要解決什么問題,因?yàn)槲覀円芾磉\(yùn)動員從轉(zhuǎn)會到上場比賽整條鏈路信息,所以從運(yùn)動員視角出發(fā)是一個(gè)合適的選擇。
第二個(gè)問題是對業(yè)務(wù)不熟悉怎么辦?因?yàn)槲覀儾皇求w育和運(yùn)動專家,并不清楚整條鏈路的業(yè)務(wù)細(xì)節(jié)。答案是梳理流程時(shí)一定要有業(yè)務(wù)專家在場,因?yàn)闆]有真實(shí)業(yè)務(wù)細(xì)節(jié),無法領(lǐng)域驅(qū)動設(shè)計(jì)。同理在互聯(lián)網(wǎng)梳理復(fù)雜業(yè)務(wù)流程時(shí),一定要有對相關(guān)業(yè)務(wù)熟悉的產(chǎn)品經(jīng)理或者運(yùn)營一起參與。
假設(shè)足球業(yè)務(wù)專家梳理出了業(yè)務(wù)流程,運(yùn)動員提出轉(zhuǎn)會,協(xié)商一致后到新俱樂部體檢,體檢通過就進(jìn)行簽約。進(jìn)入新俱樂部后進(jìn)行訓(xùn)練,訓(xùn)練指標(biāo)達(dá)標(biāo)后上場比賽,賽后參加新聞發(fā)布會。實(shí)際流程會復(fù)雜很多,本文還是著重講解方法論。

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

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

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

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

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

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

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

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

3.3.3 狀態(tài)機(jī)圖
假設(shè)一條數(shù)據(jù)有ABC三種狀態(tài),從正常業(yè)務(wù)角度來看,狀態(tài)只能從A流轉(zhuǎn)到B,再從B流轉(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),說明了哪些狀態(tài)之間可以相互流轉(zhuǎn),在實(shí)際開發(fā)中再結(jié)合狀態(tài)機(jī)代碼模式,可以解決上述狀態(tài)異常情況。回到本文實(shí)例,我們可以通過狀態(tài)機(jī)圖表示球員從提出轉(zhuǎn)會到簽約整個(gè)狀態(tài)流程。
3.4 領(lǐng)域與數(shù)據(jù)
上述章節(jié)從功能層面和流程層面進(jìn)行了系統(tǒng)分析,現(xiàn)在從數(shù)據(jù)層分析系統(tǒng),我們首先對比兩組概念:值對象與實(shí)體,領(lǐng)域?qū)ο笈c數(shù)據(jù)對象。
實(shí)體是具有唯一標(biāo)識的對象,唯一標(biāo)識會伴隨實(shí)體對象整個(gè)生命周期并且不可變更。值對象本質(zhì)上是屬性的集合,沒有唯一標(biāo)識。
領(lǐng)域?qū)ο笈c數(shù)據(jù)對象一個(gè)重要的區(qū)別是值對象存儲方式。領(lǐng)域?qū)ο笤诎祵ο蟮耐瑫r(shí)也保留了值對象的業(yè)務(wù)含義,而數(shù)據(jù)對象可以使用更加松散的結(jié)構(gòu)保存值對象,簡化數(shù)據(jù)庫設(shè)計(jì)。
現(xiàn)在我們需要管理足球運(yùn)動員基本信息和比賽數(shù)據(jù),對應(yīng)領(lǐng)域模型和數(shù)據(jù)模型應(yīng)該如何設(shè)計(jì)?姓名、身高、體重是一名運(yùn)動員本質(zhì)屬性,加上唯一編號可以對應(yīng)實(shí)體對象。跑動距離,傳球成功率,進(jìn)球數(shù)是運(yùn)動員比賽表現(xiàn),這些屬性的集合可以對應(yīng)值對象。

我們根據(jù)圖示編寫領(lǐng)域?qū)ο笈c數(shù)據(jù)對象代碼:
// 數(shù)據(jù)對象
public class FootballPlayerDO {
private Long id;
private String name;
private Integer height;
private Integer weight;
private String gamePerformance;
}
// 領(lǐng)域?qū)ο?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存儲值對象?因?yàn)槟_本化是一種拓展靈活性的方式,腳本化不僅指使用groovy、QLExpress腳本增強(qiáng)系統(tǒng)靈活性,還包括松散可擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)。數(shù)據(jù)模型抽象出了姓名、身高、體重這些基本屬性,對于頻繁變化的比賽表現(xiàn)屬性,這些屬性值可能經(jīng)常變化,甚至屬性本身也是經(jīng)常變化,可能會加上射門次數(shù),突破次數(shù)等,所以采用松散結(jié)構(gòu)進(jìn)行存儲。
如果需要根據(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è)維度。總體而言橫向擴(kuò)展的是思考廣度,縱向擴(kuò)展的是思考深度,對應(yīng)到系統(tǒng)設(shè)計(jì)而言可以總結(jié)為:縱向做隔離,橫向做編排。
我們首先分析一個(gè)下單場景。當(dāng)前有ABC三種訂單類型:A訂單價(jià)格9折,物流最大重量不能超過9公斤,不支持退款。B訂單價(jià)格8折,物流最大重量不能超過8公斤,支持退款。C訂單價(jià)格7折,物流最大重量不能超過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("超過物流最大重量");
}
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("超過物流最大重量");
}
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("超過物流最大重量");
}
orderBO.setRefundSupport(Boolean.TRUE);
}
// 保存數(shù)據(jù)
OrderDO orderDO = new OrderDO();
BeanUtils.copyProperties(orderBO, orderDO);
orderMapper.insert(orderDO);
}
}
上述代碼從功能上完全可以實(shí)現(xiàn)業(yè)務(wù)需求,但是程序員不僅要滿足功能,還需要思考代碼的可維護(hù)性。如果新增一種訂單類型,或者新增一個(gè)訂單屬性處理邏輯,那么我們就要在上述邏輯中新增代碼,如果處理不慎就會影響原有邏輯。
為了避免牽一發(fā)而動全身這種情況,設(shè)計(jì)模式中的開閉原則要求我們面向新增開放,面向修改關(guān)閉,我認(rèn)為這是設(shè)計(jì)模式中最重要的一條原則。
需求變化通過擴(kuò)展,而不是通過修改已有代碼實(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ì)模式說到底最終都是在遵循開閉原則。
如何改變平鋪直敘的思考方式?這就要為問題分析加上縱向和橫向兩個(gè)維度,我選擇使用分析矩陣方法,其中縱向表示策略,橫向表示場景:

3.5.1 縱向做隔離
縱向維度表示策略,不同策略在邏輯上和業(yè)務(wù)上應(yīng)該是隔離的,本實(shí)例包括優(yōu)惠策略、物流策略和退款策略,策略作為抽象,不同訂單類型去擴(kuò)展這個(gè)抽象,策略模式非常適合這種場景。本文詳細(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<String, DiscountStrategy> 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("無優(yōu)惠策略");
}
discountStrategy.discount(orderBO);
}
}
3.5.2 橫向做編排
橫向維度表示場景,一種訂單類型在廣義上可以認(rèn)為是一種業(yè)務(wù)場景,在場景中將獨(dú)立的策略進(jìn)行串聯(lián),模板方法設(shè)計(jì)模式適用于這種場景。
模板方法模式一般使用抽象類定義算法骨架,同時(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ù)場景也不過是簡單場景的疊加、組合和交織,無外乎也是通過縱向做隔離、橫向做編排尋求答案。

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

縱向隔離出進(jìn)攻能力池,防守能力池,門將能力池,橫向編排出前場、中場、后場、門將四個(gè)流程,在不同流程中可以任意從能力池中選擇能力進(jìn)行組合,而不是編寫冗長的判斷邏輯,顯著提升了代碼可擴(kuò)展性。
3.6 分層看架構(gòu)
系統(tǒng)架構(gòu)總體而言分為兩個(gè)層次,第一種層次是指本項(xiàng)目在整個(gè)公司位于哪一層次。持久層、緩存層、中間件、業(yè)務(wù)中臺、服務(wù)層、網(wǎng)關(guān)層、客戶端和代理層是常見的分層架構(gòu),大多數(shù)情況下業(yè)務(wù)需求最終會體現(xiàn)在服務(wù)層,不同的業(yè)務(wù)領(lǐng)域?qū)?yīng)不同的微服務(wù)。
第二種層次是指本項(xiàng)目內(nèi)部代碼的組織方式,一般可以分為接口層,訪問層,業(yè)務(wù)層,領(lǐng)域?qū)樱獠吭L問層和基礎(chǔ)層。
(1) api
接口層:提供面向外部接口聲明和DTO
(2) controller
訪問層:提供HTTP訪問入口
(3) service
業(yè)務(wù)層:提供BO對象,領(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 接口看對接
一個(gè)接口代碼編寫完成后,那么這個(gè)接口如何調(diào)用,輸入和輸出參數(shù)是什么,這些問題需要在接口文檔中得到回答。
接口文檔生成有兩種方式,第一種方式是自動生成,例如使用Swagger框架,第二種方式是手工生成。自動生成的優(yōu)點(diǎn)是代碼即文檔,還具有調(diào)試功能,在公司內(nèi)部進(jìn)行聯(lián)調(diào)時(shí)非常方便。但是如果接口是提供給外部第三方使用,那么還是需要手工編寫接口文檔。一個(gè)接口核心描述無外乎接口名稱、接口說明、輸入?yún)?shù)、輸出參數(shù),其它信息根據(jù)需要再增加。

4 文章總結(jié)
本文通過一個(gè)業(yè)務(wù)實(shí)例介紹了技術(shù)方案的七大維度:四色分領(lǐng)域、用例看功能、流程三劍客、領(lǐng)域與數(shù)據(jù)、縱橫做設(shè)計(jì)、分層看架構(gòu)、接口看對接。每個(gè)維度描述系統(tǒng)的一個(gè)側(cè)面,組合在一起最終描繪出整個(gè)系統(tǒng)。
在實(shí)際開發(fā)中如果需求不復(fù)雜,那么也不是七個(gè)維度都要體現(xiàn),而是根據(jù)實(shí)際情況取舍,能夠把方案說清楚即可,希望本文對大家有所幫助。