復(fù)雜系統(tǒng)設(shè)計(jì)原則與案例
一、復(fù)雜是軟件的本質(zhì)屬性
1.1 復(fù)雜是軟件的本質(zhì)屬性
互聯(lián)網(wǎng)經(jīng)歷了十多年的高速發(fā)展,各個(gè)領(lǐng)域方向的系統(tǒng)都已經(jīng)歷了多次升級迭代,大家在經(jīng)手這些軟件系統(tǒng)時(shí),不免感嘆現(xiàn)在軟件系統(tǒng)的復(fù)雜度,其實(shí)軟件復(fù)雜性是軟件固有的屬性,這種固有的復(fù)雜性主要由4個(gè)方面的原因造成的:
-
問題域的復(fù)雜性
-
管理開發(fā)過程的復(fù)雜性
-
隨處可變的靈活性
-
描繪離散系統(tǒng)行為的問題
上面每一個(gè)方面都有極大的挑戰(zhàn),以「問題域的復(fù)雜性」為例,現(xiàn)在以微服務(wù)架構(gòu)設(shè)計(jì)思路下的大型系統(tǒng)中,動(dòng)不動(dòng)就幾十個(gè)應(yīng)用,組合在一起就是一個(gè)復(fù)雜的系統(tǒng),而每個(gè)人只負(fù)責(zé)其中一小部分,想要了解系統(tǒng)全部的運(yùn)行狀況是很難的,哪怕一個(gè)子系統(tǒng),它包含的業(yè)務(wù)規(guī)則就巨多,因此說軟件復(fù)雜是它的本質(zhì)屬性。
1.2 對業(yè)務(wù)認(rèn)知復(fù)雜度是影響軟件復(fù)雜性的重要因素
影響軟件復(fù)雜度的因素有很多,其中「認(rèn)知復(fù)雜度」占據(jù)著很重要的因素。一提到復(fù)雜性,我們腦海里會(huì)浮出各種各樣的印象:應(yīng)用數(shù)多、代碼行數(shù)超過百萬級、業(yè)務(wù)規(guī)則復(fù)雜等,這些復(fù)雜度從本質(zhì)上來看是認(rèn)知復(fù)雜度超過了正常人的認(rèn)知范圍,比如看百萬行級的代碼與看100行代碼相比,維護(hù)10個(gè)應(yīng)用與維護(hù)1個(gè)應(yīng)用相比,兩個(gè)復(fù)雜度不是在同一個(gè)數(shù)量級上,有可能是指數(shù)級提升。認(rèn)知復(fù)雜度是軟件的本質(zhì)復(fù)雜度,從根本上規(guī)避不了,只能去理解、消化吸收,我們能做的是在理解的基礎(chǔ)上去發(fā)現(xiàn)共性的「規(guī)律」,將這些「規(guī)律」抽象出來,讓應(yīng)用層開發(fā)變得簡單。
舉當(dāng)前的例子,目前負(fù)責(zé)的是電商板塊的物流資金結(jié)算業(yè)務(wù)系統(tǒng),最開始面對的業(yè)務(wù)認(rèn)知復(fù)雜度非常高,它關(guān)聯(lián)電商交易、支付、營銷、結(jié)算、資金等領(lǐng)域,依賴業(yè)務(wù)將近100張離線表,除了要理解電商業(yè)務(wù)鏈路外,還要站在物流,財(cái)務(wù),風(fēng)控等視角把這些數(shù)據(jù)有序地組織起來,復(fù)雜度一下子就上升上來了,新人至少要花3個(gè)月的時(shí)間去消化這些業(yè)務(wù)知識。 當(dāng)進(jìn)來做了一些需求開發(fā)后,慢慢發(fā)現(xiàn)了一些規(guī)律,利用發(fā)現(xiàn)的這些規(guī)律有助于提升需求溝通、開發(fā)的效率。
二、應(yīng)對復(fù)雜性的設(shè)計(jì)方法
2.1 把握套路是應(yīng)對復(fù)雜性的根本方法
「規(guī)律」是日常開發(fā)中發(fā)現(xiàn)有共性的地方,往后再遇到可以同樣的問題可加速解決的效率。軟件復(fù)雜度伴隨著軟件研發(fā)開始就產(chǎn)生的問題,「設(shè)計(jì)原則」就是應(yīng)對復(fù)雜性過程中總結(jié)出來的規(guī)律。常見的設(shè)計(jì)原則有SOLID、GRASP、KISS、分層等,這些設(shè)計(jì)原則指導(dǎo)我們在面對復(fù)雜系統(tǒng)時(shí)應(yīng)該如何去設(shè)計(jì)。原則的東西,個(gè)人經(jīng)驗(yàn)是建立自己的認(rèn)知體系,需要有實(shí)事求是,學(xué)以致用的實(shí)踐態(tài)度。

2.2 通識規(guī)律
在經(jīng)典的設(shè)計(jì)原則之上,最終將設(shè)計(jì)原則歸類成三個(gè)方面:「職責(zé)分解」、「層次抽象」和「變化擴(kuò)展」。
2.2.1 職責(zé)分離
對職責(zé)分離有兩點(diǎn)體會(huì):一個(gè)是「你擁有什么信息就應(yīng)該承擔(dān)怎樣的職責(zé)」;另一個(gè)是「一個(gè)類只做一件事」。當(dāng)我們在討論是否是貧血模型時(shí),你可以用這個(gè)原則去檢驗(yàn),如果一類中的成員屬性操作放在另外一類中,大概率是不符合信息專家原則,舉一個(gè)簡單的例子,比如要計(jì)算物流訂單的運(yùn)費(fèi)金額,那么這個(gè)計(jì)算方法應(yīng)該是在訂單類中,而不是放在另外一個(gè)類中,因?yàn)橛唵晤愔杏杏唵蔚膯蝺r(jià)和數(shù)量。
另一點(diǎn)是出自于SOLID的單一職責(zé),它的原意是一個(gè)類只有一個(gè)變化的原因,一個(gè)類專注于做一件事的好處是可提升復(fù)用性和減少依賴,反之一個(gè)類耦合了不同的操作,修改的頻次就會(huì)變多,盡量少改動(dòng)穩(wěn)定的部分,在系統(tǒng)穩(wěn)定性中有一個(gè)共性認(rèn)知:故障的發(fā)生大概率與最近的發(fā)布有關(guān)。
職責(zé)分解最大的挑戰(zhàn)是一個(gè)職責(zé)到底要?jiǎng)澐值蕉嗉?xì)或多粗,只能說只做一件事或者只有一個(gè)變化這樣大的指導(dǎo)原則,更多的是我們在實(shí)踐中總結(jié)出來的經(jīng)驗(yàn),比如「變與不變分離」、「讀寫分離」、「配置域與執(zhí)行域分離」。
2.2.2 層次抽象
層次抽象是利用已發(fā)現(xiàn)的規(guī)律,讓往后的開發(fā)變得簡單,當(dāng)我們在一線開發(fā)中,你會(huì)發(fā)現(xiàn)有一些規(guī)律,比如在日常開發(fā)中,發(fā)現(xiàn)開發(fā)主要涉及到與前端交互、業(yè)務(wù)邏輯處理和數(shù)據(jù)存儲(chǔ),這樣就可以分成三層:「視圖層」、「業(yè)務(wù)邏輯層」和「數(shù)據(jù)訪問層」。
高層次依賴低層次,最高層次越具象,也會(huì)越簡單,舉一個(gè)例子,在傳統(tǒng)Servlet開發(fā)中,一般的步驟是獲取參數(shù)信息并轉(zhuǎn)成業(yè)務(wù)層的對象,再進(jìn)行業(yè)務(wù)處理,雖然不同的業(yè)務(wù)處理邏輯是不一樣的,但參數(shù)獲取是具有共性的操作,在SpringMVC中,我們可以直接定義POJO去映射參數(shù),可以不用使用HttpServlet底層的操作去獲取參數(shù),這就是一種典型的層次抽象。
「層次特性」是復(fù)雜系統(tǒng)的固有屬性,需要我們不斷去探索,分層的確能極大地降低認(rèn)知復(fù)雜度,相當(dāng)是站在巨人的肩膀上看問題,利用已發(fā)現(xiàn)的規(guī)律辦事效率會(huì)高很多,如上文提到的財(cái)務(wù)核算,做多了就會(huì)發(fā)現(xiàn)就那幾種模式,當(dāng)你沒有摸清里面的規(guī)律時(shí),會(huì)覺得顯得很零散。
2.2.3 變化擴(kuò)展
軟件如果沒有變化,也就不需要所謂的設(shè)計(jì)原則,一次性工程怎么快就怎么來,而現(xiàn)實(shí)中遇到最多的現(xiàn)象是需求不斷變化。變化擴(kuò)展的挑戰(zhàn)不在于技術(shù),而是在于「怎么認(rèn)知到哪里有變化」。常見變化擴(kuò)展的技術(shù)有:配置項(xiàng)、接口、抽象類、攔截器、SPI、插件等,這些都是具體的解決手段,它們并不復(fù)雜,復(fù)雜在于哪里會(huì)有變化,這個(gè)是最難的。
認(rèn)識到多少變化,它取決于認(rèn)識的寬度,看到多少內(nèi)容會(huì)影響到系統(tǒng)設(shè)計(jì),比如在SpringMVC中,我們最高常操作的是定義一個(gè)Controller,再在方法上寫一個(gè)RequestMapping注解,但在實(shí)際中,它還有另外的寫法,如實(shí)現(xiàn)Controller接口,正是有不同的場景和類型,處理上還有差別,此時(shí)就會(huì)有變化擴(kuò)展的訴求。
2.3 軟件設(shè)計(jì)的6條經(jīng)驗(yàn)
在經(jīng)典的設(shè)計(jì)原則之上,結(jié)合實(shí)踐過程中的得與失,總結(jié)了以下6條設(shè)計(jì)經(jīng)驗(yàn),為了更容易理解,下面的案例選用常用的開源框架剖析設(shè)計(jì)思想,方便與大家產(chǎn)生共鳴。
2.3.1 模板方法-在多變中找不變
當(dāng)一個(gè)業(yè)務(wù)有多個(gè)場景,并且不同的場景處理既有共性的地方,也有差異性的地方時(shí),此時(shí)最容易想到的方法是用「模板方法」固定共性的邏輯,差異性的邏輯放到子類中實(shí)現(xiàn)。在開源框架中,我們經(jīng)常見到這樣的設(shè)計(jì)思想,比如在SpringMVC中查找Handler的過程,不同的場景查找邏輯不一樣,最常見的是RequestMapping方式查找,它是在HandMapping接口類中定義getHandler方法。
public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request)
throws Exception;
}
然后在抽象類AbstractHandlerMapping中定義模板方法,抽象方法又交由子類去實(shí)現(xiàn)。
public final HandlerExecutionChain getHandler(HttpServletRequest request)
throws Exception {
// 抽象方法,交由具體的子類實(shí)現(xiàn)
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// 省略部分代碼
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
// 省略部分代碼
return executionChain;
}
在MyBatis框架中,Executor定義了增刪改查等方法,具體實(shí)現(xiàn)有如單條命令執(zhí)行、批量命令執(zhí)行等,模板方法定義在BaseExecutor類中,類結(jié)構(gòu)繼承關(guān)系如下所示,這也是一種最簡單的三層設(shè)計(jì)結(jié)構(gòu):接口類、抽象類、子類。

2.3.2 命令職責(zé)-業(yè)務(wù)鏈路查詢和復(fù)雜組裝
有一類業(yè)務(wù),它涉及「查詢」與「組裝」兩個(gè)操作,比如Spring中有Bean查詢操作,與之對應(yīng)的有Bean創(chuàng)建操作,這兩個(gè)職責(zé)是不一樣的,也有的稱之為「讀寫分離」或者「查詢與命令分離」,從本質(zhì)上講,它也遵循了接口單一職責(zé)。

2.3.3 配置域與執(zhí)行域分離-有面向用戶配置
有些業(yè)務(wù)前臺(tái)用戶能夠直接配置操作的,比如在SpringMVC中,我們配置一個(gè)Controller的請求可以配置不同的屬性,其中RequestMapping是直接面向用戶視角的配置操作,在配置域的內(nèi)容,是與現(xiàn)實(shí)操作一一映射的,RequestMapping對應(yīng)有一個(gè)類叫RequestMappingInfo,然而在執(zhí)行域,此時(shí)它就不需要配置域中的那么多信息,執(zhí)行過程只要對象和方法的信息即可,對應(yīng)有一個(gè)類中HandlerMethod,由此可見,配置域和執(zhí)行域兩個(gè)抽象的視角是不一樣的,一個(gè)是現(xiàn)實(shí)世界的直接映射,一個(gè)是偏底層執(zhí)行。
@RestController
public class UserController {
@RequestMapping(value = "/acquire", method = RequestMethod.GET)
public User getUser(@RequestParam("name") String name, @RequestParam("age") Integer age) {
return null;
}
}
RequestMappingHandlerMapping類結(jié)構(gòu)繼承關(guān)系如下圖所示。

再比如在Spring中,允許用戶配置自定義的編輯器、BeanPostProcessor處理器,也是由一個(gè)單獨(dú)的接口類ConfigurableBeanFactory表達(dá)的。
這樣的例子還有很多,比如BeanDefinition是面向配置域的,Bean是執(zhí)行域的,我們在定義Bean有很多的屬性,這些屬性信息在BeanDefinition類中定義,而在執(zhí)行過程中會(huì)生成一個(gè)對象,本質(zhì)上是一個(gè)Object。
2.3.4 封裝變化-業(yè)務(wù)有多樣變化
應(yīng)對變化的方法有很多,難的是要感知到變化并且封裝好變化,比如Spring Bean實(shí)例化后進(jìn)行初始化,在此期間就有很多操作,如常見的Bean依賴注入、AOP代理等,Spring抽象出BeanPostProcessor擴(kuò)展類,在Bean初始化前后做一些額外的擴(kuò)展工作。
public interface BeanPostProcessor {
// 初始化前的操作
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
// 初始化后的操作
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
設(shè)計(jì)擴(kuò)展點(diǎn)時(shí)一定要把握好度,粒度過細(xì)則擴(kuò)展點(diǎn)數(shù)量非常多,在Spring中設(shè)計(jì)就比較好,對于開發(fā)而言,有兩個(gè)時(shí)機(jī)有明顯的擴(kuò)展訴求,一個(gè)是在Bean掃描時(shí),可以允許用戶自定義Bean,此時(shí)有BeanFactoryPostProcessor擴(kuò)展接口;另一個(gè)是在Bean初始化時(shí)的擴(kuò)展,對應(yīng)有BeanPostProcessor擴(kuò)展接口。不管是Spring內(nèi)部使用,還是外部開發(fā),都是使用同樣的擴(kuò)展。
2.3.5 責(zé)任鏈-業(yè)務(wù)流程型操作
業(yè)務(wù)型操作,有明顯的流程痕跡,比如前置檢查、協(xié)議組裝、接口調(diào)用等,節(jié)點(diǎn)與節(jié)點(diǎn)之間就構(gòu)成了一條鏈條,只不過平時(shí)寫代碼時(shí)我們是放在一個(gè)大的流程中實(shí)現(xiàn)的。在HttpClient中,對于請求,我們有不同的操作流程,比如重試、緩存、重定向、調(diào)用socket等操作,HttpClient使用責(zé)任鏈的模式。

鏈條上的每個(gè)節(jié)點(diǎn)都是獨(dú)立操作的,方便擴(kuò)展,責(zé)任鏈核心是鏈的構(gòu)建和節(jié)點(diǎn)設(shè)計(jì),這給平時(shí)寫流程型業(yè)務(wù)代碼提供了一種新的思路,大型系統(tǒng)中,有流程引擎,本質(zhì)來講它也是一條鏈,一個(gè)節(jié)點(diǎn)做完之后下一個(gè)節(jié)點(diǎn)繼續(xù)做,思想上大同小異。
2.3.6 合理抽象-復(fù)雜系統(tǒng)場景
抽象是應(yīng)對復(fù)雜場景的重要方法,這一點(diǎn)我們并不懷疑,最難的是要抽象什么去刻畫業(yè)務(wù),比如AOP切面編程,站在用戶視角,就是告訴他哪些類、哪些方法需要被增強(qiáng)什么共性業(yè)務(wù)邏輯,比如日志切面類、權(quán)限切面類等,AOP對它的抽象是「對指定的類和方法以某種方式織入特定的共性邏輯」。其中指定的類和方法抽象成切點(diǎn),以某種方式抽象成通知。此時(shí),你會(huì)發(fā)現(xiàn)它抽象出了一些概念出來,如切面、切點(diǎn)、通知。因此,對復(fù)雜業(yè)務(wù)場景,一定要有一套抽象的元數(shù)據(jù)去表征它,也即是領(lǐng)域模型,最高明的建模方法是下定義的方法,用一句簡明的話講清楚業(yè)務(wù)的結(jié)構(gòu)和功能。

系統(tǒng)是元素和元素間以某種關(guān)聯(lián)關(guān)系構(gòu)成的一種結(jié)構(gòu),復(fù)雜系統(tǒng)是構(gòu)成元素更多、關(guān)聯(lián)關(guān)系更復(fù)雜,核心還是要找到「結(jié)構(gòu)」,這種結(jié)構(gòu)也即是領(lǐng)域模型,好的領(lǐng)域模型可遇而不可求,是要花大量的時(shí)間去探尋它,突然有一天在你腦海里靈光一現(xiàn)就出來了,這種感覺很奇妙,因此,領(lǐng)域建模是非常依賴經(jīng)驗(yàn)而非方法。
三、框架設(shè)計(jì)案例分析
有了上面的分析基礎(chǔ),再以SpringMVC DispatcherServlet為例,分析它的設(shè)計(jì)思想,它的結(jié)構(gòu)如下圖所示。

SpringMVC核心是對HttpServlet的封裝,在HttpServlet中有兩個(gè)重要的方法,一個(gè)是init()方法,一個(gè)是service()方法,init()方法是Servlet初始化時(shí)回調(diào)的方法,service()是處理請求時(shí)回調(diào)的方法。
在HttpServletBean類中,它重寫了HttpServlet init()方法,主要完成SpringMVC子容器初始化的過程。FrameworkServlet類主要重寫了service()方法,處理實(shí)際的如GET、POST請求,但它只是定義了一個(gè)抽象的doService()方法,實(shí)際處理過程是在DispatcherServlet類中,分發(fā)的Servlet是攔截所有的請求,然后匹配到目標(biāo)Handler執(zhí)行。
在DispatcherServlet類的設(shè)計(jì)中,體現(xiàn)出了「職責(zé)分離」和「變化擴(kuò)展」的設(shè)計(jì)思想,init初始化與service執(zhí)行分離,攔截器支持變化擴(kuò)展。上面列舉的幾個(gè)框架,它們都是解決了一些平常的問題,但不影響它們優(yōu)秀的設(shè)計(jì),如MyBatis、Spring、SpringMVC、HttpClient,它們并沒有在一個(gè)大類中實(shí)現(xiàn)各種各樣的功能,而是切分放在不同的類中,并且通過多層繼承關(guān)系組合在一起,不管是可讀性上,還是可擴(kuò)展性上都非常不錯(cuò)。
四、認(rèn)知是解決復(fù)雜性的基石
在認(rèn)知面前,所有的方法和工具都是蒼白的,就像一個(gè)人想不勞而獲一樣,總想找一種萬能的方法解決所有的問題,而事實(shí)并沒有,還得靠在實(shí)踐中解決問題。復(fù)雜性也是同樣的問題,沒有萬能的方法解決它,只有原則作為指導(dǎo),而具體要怎么去做,還是得身體力行。當(dāng)我們不理解框架為什么要設(shè)計(jì)得這么復(fù)雜時(shí),大概率是我們對應(yīng)用的場景了解還不夠全面。
4.1 業(yè)務(wù)認(rèn)知
當(dāng)大家第一次去看Spring Bean掃描的邏輯時(shí),它的邏輯是很復(fù)雜的,如果讓我們自己去實(shí)現(xiàn)一個(gè),你可能會(huì)很簡單的設(shè)計(jì)出來,根據(jù)指定的路徑掃描所有的類,如果有@Component的注解時(shí)就存放到BeanDefinnitionMap中,那為什么Spring要設(shè)計(jì)得這么復(fù)雜呢,原因是現(xiàn)實(shí)場景中Bean定義有多種方法,比如嵌套定義Bean,再比如先掃描出一部分Bean,此時(shí)這些Bean中有定義@CompentScan,又可以加載其它的Bean,所以你看這么多你不曾考慮的場景疊加在一起,實(shí)現(xiàn)起來的復(fù)雜度自然就高了。
還比如SpringMVC在查找Handler時(shí),它的邏輯也挺復(fù)雜的,與我們?nèi)粘Mㄟ^一個(gè)URL映射到一個(gè)Handler不一樣,在現(xiàn)實(shí)中完全有一種可能是相同的URL對應(yīng)不同的請求方法,此時(shí)就不是一個(gè)簡單映射的就能完成,還有一大堆的匹配邏輯,所以你會(huì)看到,當(dāng)我們的業(yè)務(wù)認(rèn)知了解得越來越多時(shí),在設(shè)計(jì)中就會(huì)考慮更多的因素。
提升業(yè)務(wù)認(rèn)知,除了溝通交流外,還得踏踏實(shí)實(shí)去工作一段時(shí)間,真正地了解里面的問題是什么,即使是踩坑,也是修正自己的認(rèn)知。
4.2 技術(shù)認(rèn)知
除了業(yè)務(wù)認(rèn)知外,技術(shù)也是在不斷發(fā)展的,如果你不了解某個(gè)技術(shù)或技術(shù)點(diǎn),此時(shí)你也不會(huì)想到好的設(shè)計(jì)方法。比如讓你設(shè)計(jì)一個(gè)事件通知框架,本來這個(gè)功能倒不是那么復(fù)雜,它最難的點(diǎn)是在于如何找到事件對應(yīng)的事件處理器,此時(shí)就有不同的解決方案,一種最簡單的方法是在定義事件處理器時(shí)讓用戶指定事件類型,這似乎是一種解決方案,但站在用戶使用的角度看,它并不是一種好的解決方案,把復(fù)雜留給用戶而不是自己。為了提升用戶使用體驗(yàn),這里就要使用到泛型類型解析的方面的知識了,核心代碼如下:
/**
* 事件分發(fā)器
*
* @author fulai.gfl
*/
public class EventDispatcher {
/**
* 事件列表
*/
private static List<Event> events = new ArrayList<>();
/**
* 事件處理器列表
*/
private static List<Handler> handlers = new ArrayList<>();
/**
* 添加事件
*/
public static void addEvent(Event event) {
events.add(event);
}
/**
* 添加事件處理器
*/
public static void addHandler(Handler handler) {
handlers.add(handler);
}
/**
* 觸發(fā)事件
*/
public Object fire(Event event) throws Exception {
Handler handler = getHandler(event);
if(Objects.isNull(handler)){
throw new Exception("event_name =" + event.getEventName());
}
return handler.handle(event);
}
/**
* 根據(jù)事件找到對應(yīng)的Handler
*/
private Handler getHandler(Event event) throws Exception {
Handler handler = null;
for (Handler h : handlers) {
Type[] argumentsTypes =
((ParameterizedTypeImpl)h.getClass()
.getGenericInterfaces()[0])
.getActualTypeArguments();
if (Class.forName(((Class)argumentsTypes[0])
.getName()).equals(event.getClass())) {
handler = h;
}
}
return handler;
}
}
五、小結(jié)
本文主要講述了應(yīng)對復(fù)雜性的一些原則和經(jīng)驗(yàn),通過實(shí)際案例解構(gòu)設(shè)計(jì)思想,個(gè)人認(rèn)為好的設(shè)計(jì)是體現(xiàn)在「職責(zé)分離」、「抽象分層」和「變化擴(kuò)展」上,在類的結(jié)構(gòu)設(shè)計(jì)上尤其要花心思去想,如「變與不變分離」、「配置域與執(zhí)行域分離」、「查詢與命令分離」。歸根到底,認(rèn)知是解決復(fù)雜性的基石,如果要更好地發(fā)揮技術(shù)的作用,對業(yè)務(wù)的理解需要更好的認(rèn)識。
