1 為什么要寫技術方案
回顧軟件開發的歷史進程,我們可以將其分為程序設計時代、程序系統時代和軟件工程時代三大歷史階段。
在程序設計時代(1946-1956),軟件開發主要依賴于個人編程技巧,技術文檔只要存在個人開發者的大腦即可,因為沒有溝通和協作需要,編寫技術文檔也不具有緊迫性。
在程序系統時代(1956-1968),計算機性能顯著提升,應用范圍和規模逐步擴大,以至于依靠個人無法完成軟件的開發,所以出現了團隊合作。在早期團隊合作過程中,開發者仍然保持了早期各自為戰的開發習慣,即使出現了一些方法論雛形,也無法從根本上控制溝通和協作的巨大成本,軟件危機就此出現。1968年國際學術會議提出了軟件危機和軟件工程的概念。
軟件危機的定義是落后的軟件生產方式無法滿足迅速增長的計算機軟件需求,從而導致開發與維護過程中出現一系列嚴重問題的現象。軟件的工程定義是建立并使用完善的工程化原則,以較經濟的手段獲得能在實際機器上有效運行的可靠軟件的一系列方法
從此軟件開發進入工程化階段,也應運而生了大量開發方法論和開發模型。其中標準和完善的文檔是軟件工程重要組成部分,可以很大程度上減少溝通和協作成本,而技術方案又是技術文檔重要組成部分。
2 技術方案要體現什么
軟件系統生命周期包括定義、開發、運維、消亡這四大階段。定義階段包括定義問題、可行性研究和需求分析。開發階段包括概要設計、詳細設計、編碼和測試。運維階段包括更正性維護、適應性維護、預防性維護和完善性維護。消亡階段包括系統報廢和優雅下線。

生命周期每個階段固然有各自的重要性,但是開發者更應該關注定義階段與開發階段。定義階段需要解決為什么開發(why)、需求是什么(what)兩個問題,開發階段需要解決怎么設計,怎么編碼,怎么測試(how)三個問題。
技術方案是否需要體現定義和開發的所有子階段?我認為也無必要。問題定義和可行性研究主要由產品經理負責,測試階段主要由測試人員負責,開發者可以關注但不是必須體現在技術方案。我認為技術方案必須要體現需求分析、概要設計、詳細設計、編碼四個子階段。
3 七大維度
我認為一份完整技術方案應該至少具有七大維度,每個維度描述系統的一個側面,組合在一起最終描繪出整個系統,這些維度分別是:
四色分領域
用例看功能
流程三劍客
領域與數據
縱橫做設計
分層看架構
接口看對接
本文我們分析一個足球運動員信息管理系統,這個系統我們可能也都沒有做過,正好一起分析這個系統。需要說明本文著重介紹方法論的落地,業務細節難以面面俱到。
3.1 四色分領域
3.1.1 流程梳理
首先梳理業務流程,這里有兩個問題需要考慮,第一個問題是從什么視角去梳理?因為不同的人看到的流程是不一樣的。答案是取決于系統需要解決什么問題,因為我們要管理運動員從轉會到上場比賽整條鏈路信息,所以從運動員視角出發是一個合適的選擇。
第二個問題是對業務不熟悉怎么辦?因為我們不是體育和運動專家,并不清楚整條鏈路的業務細節。答案是梳理流程時一定要有業務專家在場,因為沒有真實業務細節,無法領域驅動設計。同理在互聯網梳理復雜業務流程時,一定要有對相關業務熟悉的產品經理或者運營一起參與。
假設足球業務專家梳理出了業務流程,運動員提出轉會,協商一致后到新俱樂部體檢,體檢通過就進行簽約。進入新俱樂部后進行訓練,訓練指標達標后上場比賽,賽后參加新聞發布會。實際流程會復雜很多,本文還是著重講解方法論。

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

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

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

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

3.1.3 劃分領域
在四色建模過程中我們體會到時標對象是最重要的對象,因為其承載了業務系統核心單據。在劃分領域時我們同樣離不開時標對象,通過收斂相關時標對象劃分領域。

3.1.4 領域事件
當業務系統發生一件事情時,如果本領域或其它領域有后續動作跟進,那么我們把這件事情稱為領域事件,這個事件需要被感知。
例如球員比賽受傷,這是比賽子域事件,但是醫療和訓練子域是需要感知的,那么比賽子域就發出一個事件,醫療和訓練子域會訂閱。球員比賽取得進球,這也是比賽子域事件,但是訓練和合同子域也會關注這個事件,所以比賽子域也會發出一個比賽進球事件,訓練和合同子域會訂閱。
通過事件交互有一個問題需要注意,通過事件訂閱實現業務只能采用最終一致性,需要放棄強一致性,可能會引入新的復雜度需要權衡。

3.2 用例看功能
目前為止領域已經確定了,大領域已經拆分成了小領域,我們已經不再束手無策,而是可以對小領域進行用例分析了。用例圖由參與者和用例組成,目的是回答這樣一個問題:什么人使用系統干什么事。
下圖表示在比賽領域,運動員視角(什么人)使用系統進行進球統計,助攻統計,犯規統計,跑動距離統計,比賽評分統計,傳球成功率統計,受傷統計(干什么事),同理也可以選擇四色建模中其它參與者視角繪制用例圖。
include關鍵字表示包含關系。例如比賽是基用例,包含了進球統計,助攻統計,犯規統計,跑動距離統計,比賽評分統計,傳球成功率統計,受傷統計七個子用例。包含關系表示法有兩個優點:第一是可以清晰地組織子用例,第二是有利于子用例復用,例如主教練視角用例圖也包含比賽評分,那么就可以直接指向比賽評分子用例。
extend關鍵字表示擴展關系。例如點球統計是進球統計的擴展,因為不一定可以獲得點球,所以點球統計即使不存在,也不會影響進球統計功能。黃牌統計、紅牌統計是犯規統計的擴展,因為普通犯規不會獲得紅黃牌,所以紅黃牌統計不存在,也不會影響犯規統計功能。
用例圖不關心實現細節,而是從外部視角描述系統功能,即使不了解實現細節的人,通過看用例圖也可以快速了解系統功能,這個特性規定了用例圖不宜過于復雜,能夠說明核心功能即可。
3.3 流程三劍客
用例圖是從外部視角描述系統,但是分析系統總是要深入系統內部的,其中流程視圖就是描述系統內如何流轉的視圖。
活動圖、序列圖、狀態機圖是流程視圖中最重要的三種視圖,我們稱為流程三劍客。三者側重點有所不同:活動圖側重于邏輯分支,順序圖側重于交互,狀態機圖側重于狀態流轉。
3.3.1 活動圖
活動圖適合描述復雜邏輯分支,設想這樣一種業務場景,球隊需要選拔一名球員成為足球先生,選拔標準如下:前場、中場、后場、門將各選出一名候選球員。前場隊員依次比較進球數、助攻數,中場隊員依次比較助攻數、搶斷數,后場隊員依次比較解圍數、搶斷數,門將依次比較撲救數、撲點數,如果所有指標均相同則抽簽。每個位置有人選之后,全體教練組投票,如果投票數相同則抽簽。
業界流傳著一句話:一圖勝千言,其中一個重要原因是文字是線性的,所以表達邏輯分支能力不如流程視圖,而在流程視圖中表達邏輯分支能力最強正是活動圖。

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

3.3.3 狀態機圖
假設一條數據有ABC三種狀態,從正常業務角度來看,狀態只能從A流轉到B,再從B流轉到C,不能亂序也不可逆。但是可能出現這種異常情況:數據當前狀態為A,接收異步消息更改狀態,B消息由于延時晚于C消息,最終導致狀態先改為C再改為B,那么此時狀態就是錯誤的。
狀態機圖側重于狀態流轉,說明了哪些狀態之間可以相互流轉,在實際開發中再結合狀態機代碼模式,可以解決上述狀態異常情況。回到本文實例,我們可以通過狀態機圖表示球員從提出轉會到簽約整個狀態流程。
3.4 領域與數據
上述章節從功能層面和流程層面進行了系統分析,現在從數據層分析系統,我們首先對比兩組概念:值對象與實體,領域對象與數據對象。
實體是具有唯一標識的對象,唯一標識會伴隨實體對象整個生命周期并且不可變更。值對象本質上是屬性的集合,沒有唯一標識。
領域對象與數據對象一個重要的區別是值對象存儲方式。領域對象在包含值對象的同時也保留了值對象的業務含義,而數據對象可以使用更加松散的結構保存值對象,簡化數據庫設計。
現在我們需要管理足球運動員基本信息和比賽數據,對應領域模型和數據模型應該如何設計?姓名、身高、體重是一名運動員本質屬性,加上唯一編號可以對應實體對象。跑動距離,傳球成功率,進球數是運動員比賽表現,這些屬性的集合可以對應值對象。

我們根據圖示編寫領域對象與數據對象代碼:
// 數據對象
public class FootballPlayerDO {
private Long id;
private String name;
private Integer height;
private Integer weight;
private String gamePerformance;
}
// 領域對象
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存儲值對象?因為腳本化是一種拓展靈活性的方式,腳本化不僅指使用groovy、QLExpress腳本增強系統靈活性,還包括松散可擴展的數據結構。數據模型抽象出了姓名、身高、體重這些基本屬性,對于頻繁變化的比賽表現屬性,這些屬性值可能經常變化,甚至屬性本身也是經常變化,可能會加上射門次數,突破次數等,所以采用松散結構進行存儲。
如果需要根據JSON結構中KEY進行檢索,例如查詢進球數大于5的球員,這也不是沒有辦法。我們可以將MySQL表中數據平鋪到ES中,一條數據根據JSON KEY平鋪變成為多條數據,這樣就可以進行檢索了。
3.5 縱橫做設計
復雜業務之所以復雜,一個重要原因是涉及角色或者類型較多,很難平鋪直敘地進行設計,所以我們需要增加分析維度。其中最常見的是增加橫向和縱向兩個維度,本文也著重討論兩個維度。總體而言橫向擴展的是思考廣度,縱向擴展的是思考深度,對應到系統設計而言可以總結為:縱向做隔離,橫向做編排。
我們首先分析一個下單場景。當前有ABC三種訂單類型:A訂單價格9折,物流最大重量不能超過9公斤,不支持退款。B訂單價格8折,物流最大重量不能超過8公斤,支持退款。C訂單價格7折,物流最大重量不能超過7公斤,支持退款。按照需求字面含義平鋪直敘地寫代碼也并不難:
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMApper orderMapper;
@Override
public void createOrder(OrderBO orderBO) {
if (null == orderBO) {
throw new RuntimeException("參數異常");
}
if (OrderTypeEnum.isNotValid(orderBO.getType())) {
throw new RuntimeException("參數異常");
}
// 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);
}
// 保存數據
OrderDO orderDO = new OrderDO();
BeanUtils.copyProperties(orderBO, orderDO);
orderMapper.insert(orderDO);
}
}
上述代碼從功能上完全可以實現業務需求,但是程序員不僅要滿足功能,還需要思考代碼的可維護性。如果新增一種訂單類型,或者新增一個訂單屬性處理邏輯,那么我們就要在上述邏輯中新增代碼,如果處理不慎就會影響原有邏輯。
為了避免牽一發而動全身這種情況,設計模式中的開閉原則要求我們面向新增開放,面向修改關閉,我認為這是設計模式中最重要的一條原則。
需求變化通過擴展,而不是通過修改已有代碼實現,這樣就保證代碼穩定性。擴展也不是隨意擴展,因為事先定義了算法,擴展也是根據算法擴展,用抽象構建框架,用實現擴展細節。標準意義的二十三種設計模式說到底最終都是在遵循開閉原則。
如何改變平鋪直敘的思考方式?這就要為問題分析加上縱向和橫向兩個維度,我選擇使用分析矩陣方法,其中縱向表示策略,橫向表示場景:

3.5.1 縱向做隔離
縱向維度表示策略,不同策略在邏輯上和業務上應該是隔離的,本實例包括優惠策略、物流策略和退款策略,策略作為抽象,不同訂單類型去擴展這個抽象,策略模式非常適合這種場景。本文詳細分析優惠策略,物流策略和退款策略同理。
// 優惠策略
public interface DiscountStrategy {
public void discount(OrderBO orderBO);
}
// A類型優惠策略
@Component
public class TypeADiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.9);
}
}
// B類型優惠策略
@Component
public class TypeBDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.8);
}
}
// C類型優惠策略
@Component
public class TypeCDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.7);
}
}
// 優惠策略工廠
@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);
}
}
// 優惠策略執行
@Component
public class DiscountStrategyExecutor {
private DiscountStrategyFactory discountStrategyFactory;
public void discount(OrderBO orderBO) {
DiscountStrategy discountStrategy = discountStrategyFactory.getStrategy(orderBO.getType());
if (null == discountStrategy) {
throw new RuntimeException("無優惠策略");
}
discountStrategy.discount(orderBO);
}
}
3.5.2 橫向做編排
橫向維度表示場景,一種訂單類型在廣義上可以認為是一種業務場景,在場景中將獨立的策略進行串聯,模板方法設計模式適用于這種場景。
模板方法模式一般使用抽象類定義算法骨架,同時定義一些抽象方法,這些抽象方法延遲到子類實現,這樣子類不僅遵守了算法骨架約定,也實現了自己的算法。既保證了規約也兼顧靈活性,這就是用抽象構建框架,用實現擴展細節。
// 創建訂單服務
public interface CreateOrderService {
public void createOrder(OrderBO orderBO);
}
// 抽象創建訂單流程
public abstract class AbstractCreateOrderFlow {
@Resource
private OrderMapper orderMapper;
public void createOrder(OrderBO orderBO) {
// 參數校驗
if (null == orderBO) {
throw new RuntimeException("參數異常");
}
if (OrderTypeEnum.isNotValid(orderBO.getType())) {
throw new RuntimeException("參數異常");
}
// 計算優惠
discount(orderBO);
// 計算重量
weighing(orderBO);
// 退款支持
supportRefund(orderBO);
// 保存數據
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);
}
// 實現創建訂單流程
@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一樣并行執行,提升執行效率。
此時我們回到本文足球運動員管理系統,如果采用縱向和橫向思維分析3.3.1足球先生選拔業務場景可以得到下圖:

縱向隔離出進攻能力池,防守能力池,門將能力池,橫向編排出前場、中場、后場、門將四個流程,在不同流程中可以任意從能力池中選擇能力進行組合,而不是編寫冗長的判斷邏輯,顯著提升了代碼可擴展性。
3.6 分層看架構
系統架構總體而言分為兩個層次,第一種層次是指本項目在整個公司位于哪一層次。持久層、緩存層、中間件、業務中臺、服務層、網關層、客戶端和代理層是常見的分層架構,大多數情況下業務需求最終會體現在服務層,不同的業務領域對應不同的微服務。
第二種層次是指本項目內部代碼的組織方式,一般可以分為接口層,訪問層,業務層,領域層,外部訪問層和基礎層。
(1) api
接口層:提供面向外部接口聲明和DTO
(2) controller
訪問層:提供HTTP訪問入口
(3) service
業務層:提供BO對象,領域層和業務層都包含業務,但是用途不同。業務層可以組合不同領域業務,并且可以增加流控、監控、日志、權限控制切面,相較于領域層更為豐富
(4) domain
領域層:提供DMO、VO、事件、DO和數據訪問,核心是根據領域進行分包,領域內高內聚,領域間低耦合
(5) dependency
外部訪問層:在這個模塊中調用外部RPC服務,解析返回碼和返回數據
(6) infrastructure
基礎層:包含通用基礎功能,例如基礎工具,緩存工具,打印日志,消息發送

本文僅展開領域層進行分析。領域層核心是按照領域進行分包,并且提供DMO、VO、事件、DO和數據訪問,領域內高內聚,領域間低耦合。
3.7 接口看對接
一個接口代碼編寫完成后,那么這個接口如何調用,輸入和輸出參數是什么,這些問題需要在接口文檔中得到回答。
接口文檔生成有兩種方式,第一種方式是自動生成,例如使用Swagger框架,第二種方式是手工生成。自動生成的優點是代碼即文檔,還具有調試功能,在公司內部進行聯調時非常方便。但是如果接口是提供給外部第三方使用,那么還是需要手工編寫接口文檔。一個接口核心描述無外乎接口名稱、接口說明、輸入參數、輸出參數,其它信息根據需要再增加。

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