
作者:范鋼 曾任航天信息首席架構師,《大話重構》一書的作者。
2004 年,軟件大師 Eric Evans 的不朽著作《領域驅動設計:軟件核心復雜性應對之道》面世,從書名可以看出,這是一本應對軟件系統(tǒng)越來越復雜的方法論的圖書。
然而,在當時,中國的軟件業(yè)才剛剛起步,軟件系統(tǒng)還沒有那么復雜,即使維護了幾年,軟件退化了,不好維護了,推倒重新開發(fā)就好了。因此,在過去的那么多年里,真正運用領域驅動設計開發(fā)(DDD)的團隊并不多。一套優(yōu)秀的方法論,因為現(xiàn)實階段的原因而一直不溫不火。
不過,這些年隨著中國軟件業(yè)的快速發(fā)展,軟件規(guī)模越來越大,生命周期也越來越長,推倒重新開發(fā)的成本和風險越來越大。這時,軟件團隊急切需要在較低成本的狀態(tài)下持續(xù)維護一個系統(tǒng)很多年。然而,事與愿違。隨著時間的推移,程序越來越亂,維護成本越來越高,軟件退化成了無數(shù)軟件團隊的噩夢。
這時,微服務架構成了規(guī)?;浖慕鉀Q之道。不過,微服務對設計提出了很高的要求,強調“小而專、高內聚”,否則就不能發(fā)揮出微服務的優(yōu)勢,甚至可能令問題更糟糕。
因此,微服務的設計,微服務的拆分都需要領域驅動設計的指導。那么,領域驅動為什么能解決軟件規(guī)?;膯栴}呢?我們先從問題的根源談起,即軟件退化。
最近 10 年的互聯(lián)網發(fā)展,從電子商務到移動互聯(lián),再到“互聯(lián)網+”與傳統(tǒng)行業(yè)的互聯(lián)網轉型,是一個非常痛苦的轉型過程。而近幾年的人工智能與 5G 技術的發(fā)展,又會帶動整個產業(yè)向著大數(shù)據與物聯(lián)網發(fā)展,另一輪的技術轉型已經拉開帷幕。
那么,在這個過程中,一方面會給我們帶來諸多的挑戰(zhàn),另一方面又會給我們帶來無盡的機會,它會帶來更多的新興市場、新興產業(yè)與全新業(yè)務,給我們帶來全新的發(fā)展機遇。
然而,在面對全新業(yè)務、全新增長點的時候,我們能不能把握住這樣的機遇呢?我們期望能把握住,但每次回到現(xiàn)實,回到正在維護的系統(tǒng)時,卻令人沮喪。我們的軟件總是經歷著這樣的輪回,軟件設計質量最高的時候是第一次設計的那個版本,當?shù)谝粋€版本設計上線以后就開始各種需求變更,這常常又會打亂原有的設計。
因此,需求變更一次,版本迭代一次,軟件就修改一次,軟件修改一次,質量就下降一次。不論第一次的設計質量有多高,軟件經歷不了幾次變更,就進入一種低質量、難以維護的狀態(tài)。進而,團隊就不得不在這樣的狀態(tài)下,以高成本的方式不斷地維護下去,維護很多年。
這時候,維護好原有的業(yè)務都非常不易,又如何再去期望未來更多的全新業(yè)務呢?比如,這是一段電商網站支付功能的設計,最初的版本設計質量還是不錯的:

當?shù)谝粋€版本上線以后,很快就迎來了第一次變更,變更的需求是增加商品折扣功能,并且這個折扣功能還要分為限時折扣、限量折扣、某類商品的折扣、某個商品的折扣。當我們拿到這個需求時怎么做呢?很簡單,增加一個 if 語句,if 限時折扣就怎么怎么樣,if 限量折扣就怎么怎么樣……代碼開始膨脹了。
接著,第二次變更需要增加 VIP 會員,除了增加各種金卡、銀卡的折扣,還要為會員發(fā)放各種福利,讓會員享受各種特權。為了實現(xiàn)這些需求,我們又要在 payoff() 方法中加入更多的代碼。
第三次變更增加的是支付方式,除了支付寶支付,還要增加微信支付、各種銀行卡支付、各種支付平臺支付,此時又要塞入一大堆代碼。
經過這三次變更,你可以想象現(xiàn)在的 payoff() 方法是什么樣子了吧,變更是不是就可以結束了呢?其實不能,接著還要增加更多的秒殺、預訂、閃購、眾籌,以及各種返券。程序變得越來越亂而難以閱讀和維護,每次變更也變得越來越困難。

問題來了:為什么軟件會退化,會隨著變更而設計質量下降呢?在這個問題上,我們必須尋找到問題的根源,才能對癥下藥、解決問題。
要探尋軟件退化的根源,先要從探尋軟件的本質及其規(guī)律開始,軟件的本質就是對真實世界的模擬,每個軟件都能在真實世界中找到它的影子。因此,軟件中業(yè)務邏輯正確與否的唯一標準就是是否與真實世界一致。如果一致,則軟件是 OK 的;不一致,則用戶會提 Bug、提新需求。
在這里發(fā)現(xiàn)了一個非常重要的線索,那就是,軟件要做成什么樣,既不由我們來決定,也不由用戶來決定,而是由客觀世界決定。用戶為什么總在改需求,是因為他們也不確定客觀世界的規(guī)則,只有遇到問題了他們才能想得起來。因此,對于我們來說,與其唯唯諾諾地按照用戶的要求去做軟件,不如在充分理解業(yè)務的基礎上去分析軟件,這樣會更有利于我們減少軟件維護的成本。
那么,真實世界是怎樣的,我們就怎樣開發(fā)軟件,不就簡單了嗎?其實并非如此,因為真實世界是非常復雜的,要深刻理解真實世界中的這些業(yè)務邏輯是需要一個過程的。因此,我們最初只能認識真實世界中那些簡單、清晰、易于理解的業(yè)務邏輯,把它們做到我們的軟件里,即每個軟件的第一個版本的需求總是那么清晰明了、易于設計。
然而,當我們把第一個版本的軟件交付用戶使用的時候,用戶卻會發(fā)現(xiàn),還有很多不簡單、不明了、不易于理解的業(yè)務邏輯沒做到軟件里。這在使用軟件的過程中很不方便,和真實業(yè)務不一致,因此用戶就會提 Bug、提新需求。
在我們不斷地修復 Bug,實現(xiàn)新需求的過程中,軟件的業(yè)務邏輯也會越來越接近真實世界,使得我們的軟件越來越專業(yè),讓用戶感覺越來越好用。但是,在軟件越來越接近真實世界的過程中,業(yè)務邏輯就會變得越來越復雜,軟件規(guī)模也越來越龐大。
你一定有這樣一個認識:簡單軟件有簡單軟件的設計,復雜軟件有復雜軟件的設計。
比如,現(xiàn)在的需求就是將用戶訂單按照“單價 × 數(shù)量”公式來計算應付金額,那么在一個 PaymentBus 類中增加一個 payoff() 方法即可,這樣的設計沒有問題。不過,如果現(xiàn)在需要在付款的過程中計算各種折扣、各種優(yōu)惠、各種返券,那么我們必然會做成一個復雜的程序結構。

但是,真實情況卻不是這樣的。真實情況是,起初我們拿到的需求是那個簡單需求,然后在簡單需求的基礎上進行了設計開發(fā)。但隨著軟件的不斷變更,軟件業(yè)務邏輯變得越來越復雜,軟件規(guī)模不斷擴大,逐漸由一個簡單軟件轉變成一個復雜軟件。
這時,如果要保持軟件設計質量不退化,就應當逐步調整軟件的程序結構,逐漸由簡單的程序結構轉變?yōu)閺碗s的程序結構。如果我們總是這樣做,就能始終保持軟件的設計質量。不過非常遺憾的是,我們以往在維護軟件的過程中卻不是這樣做的,而是不斷地在原有簡單軟件的程序結構下,往 payoff() 方法中塞代碼,這樣做必然會造成軟件的退化。
也就是說,軟件退化的根源不是版本迭代和需求變更,版本迭代和需求變更只是一個誘因。如果每次軟件變更時,適時地進行解耦,進行功能擴展,再實現(xiàn)新的功能,就能保持高質量的軟件設計。但如果在每次軟件變更時沒有調整程序結構,而是在原有的程序結構上不斷地塞代碼,軟件就會退化。這就是軟件發(fā)展的規(guī)律,軟件退化的根源。
前面談到,要保持軟件設計質量不退化,必須在每次需求變更的時候,對原有的程序結構適當?shù)剡M行調整。那么應當怎樣進行調整呢?還是回到前面電商網站付款功能的那個案例,看看每次需求變更應當怎樣設計。
在交付第一個版本的基礎上,很快第一次需求變更就到來了。第一次需求變更的內容如下。
增加商品折扣功能,該功能分為以下幾種類型:
以往我們拿到這個需求,就很不冷靜地開始改代碼,修改成了如下一段代碼:這里增加了的 if else 語句,并不是一種好的變更方式。如果每次都這樣變更,那么軟件必然就會退化,進入難以維護的狀態(tài)。這種變更為什么不好呢?因為它違反了“開放-封閉原則”。開閉原則(OCP) 分為開放原則與封閉原則兩部分。- 開放原則:我們開發(fā)的軟件系統(tǒng),對于功能擴展是開放的(Open for Extension),即當系統(tǒng)需求發(fā)生變更時,可以對軟件功能進行擴展,使其滿足用戶新的需求。
- 封閉原則:對軟件代碼的修改應當是封閉的(Close for Modification),即在修改軟件的同時,不要影響到系統(tǒng)原有的功能,所以應當在不修改原有代碼的基礎上實現(xiàn)新的功能。也就是說,在增加新功能的時候,新代碼與老代碼應當隔離,不能在同一個類、同一個方法中。
前面的設計,在實現(xiàn)新功能的同時,新代碼與老代碼在同一個類、同一個方法中了,違反了“開閉原則”。怎樣才能既滿足“開閉原則”,又能夠實現(xiàn)新功能呢?在原有的代碼上你發(fā)現(xiàn)什么都做不了!難道“開閉原則”錯了嗎?問題的關鍵就在于,當我們在實現(xiàn)新需求時,應當采用“兩頂帽子”的方式進行設計,這種方式就要求在每次變更時,將變更分為兩個步驟。- 在不添加新功能的前提下,重構代碼,調整原有程序結構,以適應新功能;
按以上案例為例,為了實現(xiàn)新的功能,我們在原有代碼的基礎上,在不添加新功能的前提下調整原有程序結構,我們抽取出了 Strategy 這樣一個接口和“不折扣”這個實現(xiàn)類。這時,原有程序變了嗎?沒有。但是程序結構卻變了,增加了這樣一個接口,稱之為“可擴展點”。在這個可擴展點的基礎上再實現(xiàn)各種折扣,既能滿足“開放-封閉原則”來保證程序質量,又能夠滿足新的需求。當日后發(fā)生新的變更時,什么類型的折扣有變化就修改哪個實現(xiàn)類,添加新的折扣類型就增加新的實現(xiàn)類,維護成本得到降低。“兩頂帽子”的設計方式意義重大。過去,我們每次在設計軟件時總是擔心日后的變更,就很不冷靜地設計了很多所謂的“靈活設計”。然而,每一種“靈活設計”只能應對一種需求變更,而我們又不是先知,不知道日后會發(fā)生什么樣的變更。最后的結果就是,我們期望的變更并沒有發(fā)生,所做的設計都變成了擺設,它既不起什么作用,還增加了程序復雜度;我們沒有期望的變更發(fā)生了,原有的程序依然不能解決新的需求,程序又被打回了原形。因此,這樣的設計不能真正解決未來變更的問題,被稱為“過度設計”。有了“兩頂帽子”,我們不再需要焦慮,不再需要過度設計,正確的思路應當是“活在今天的格子里做今天的事兒”,也就是為當前的需求進行設計,使其剛剛滿足當前的需求。所謂的“高質量的軟件設計”就是要掌握一個平衡,一方面要滿足當前的需求,另一方面要讓設計剛剛滿足需求,從而使設計最簡化、代碼最少。這樣做,不僅軟件設計質量提高了,設計難點也得到了大幅度降低。簡而言之,保持軟件設計不退化的關鍵在于每次需求變更的設計,只有保證每次需求變更時做出正確的設計,才能保證軟件以一種良性循環(huán)的方式不斷維護下去。這種正確的設計方式就是“兩頂帽子”。但是,在實踐“兩頂帽子”的過程中,比較困難的是第一步。在不添加新功能的前提下,如何重構代碼,如何調整原有程序結構,以適應新功能,這是有難度的。很多時候,第一次變更、第二次變更、第三次變更,這些事情還能想清楚;但經歷了第十次變更、第二十次變更、第三十次變更,這些事情就想不清楚了,設計開始迷失方向。那么,有沒有一種方法,讓我們在第十次變更、第二十次變更、第三十次變更時,依然能夠找到正確的設計呢?有,那就是“領域驅動設計”。前面談到,軟件的本質就是對真實世界的模擬。因此,我們會有一種想法,能不能將軟件設計與真實世界對應起來,真實世界是什么樣子,那么軟件世界就怎么設計。如果是這樣的話,那么在每次需求變更時,將變更還原到真實世界中,看看真實世界是什么樣子的,根據真實世界進行變更。這樣,日后不論怎么變更,經過多少輪變更,都按照這樣的方法進行設計,就不會迷失方向,設計質量就可以得到保證,這就是“領域驅動設計”的思想。
那么,如何將真實世界與軟件世界對應起來呢?這樣的對應就包括以下三個方面的內容:- 真實世界中這些事物都有哪些行為,軟件世界中這些對象就有哪些方法;
- 真實世界中這些事物間都有哪些關系,軟件世界中這些對象間就有什么關聯(lián)。
在領域驅動設計中,就將以上三個對應,先做成一個領域模型,然后通過這個領域模型指導程序設計;在每次需求變更時,先將需求還原到領域模型中分析,根據領域模型背后的真實世界進行變更,然后根據領域模型的變更指導軟件的變更,設計質量就可以得到提高。現(xiàn)在,我們以電商網站的支付功能為例,來演練一下基于 DDD 的軟件設計及其變更的過程。
開發(fā)人員在最開始收到的關于用戶付款功能的需求描述是這樣的:以往當拿到這個需求時,開發(fā)人員往往草草設計以后就開始編碼,設計質量也就不高。而采用領域驅動的方式,在拿到新需求以后,應當先進行需求分析,設計領域模型。按照以上業(yè)務場景,可以分析出:- 一個用戶可以有多個用戶地址,但每個訂單只能有一個用戶地址;
- 此外,一個訂單對應多個訂單明細,每個訂單明細對應一個商品,每個商品對應一個供應商。
最后,我們對訂單可以進行“下單”“付款”“查看訂單狀態(tài)”等操作。因此形成了以下領域模型圖:有了這樣的領域模型,就可以通過該模型進行以下程序設計:通過領域模型的指導,將“訂單”分為訂單 Service 與值對象,將“用戶”分為用戶 Service 與值對象,將“商品”分為商品 Service 與值對象……然后,在此基礎上實現(xiàn)各自的方法。當電商網站的付款功能按照領域模型完成了第一個版本的設計后,很快就迎來了第一次需求變更,即增加折扣功能,并且該折扣功能分為限時折扣、限量折扣、某類商品的折扣、某個商品的折扣與不折扣。當我們拿到這個需求時應當怎樣設計呢?很顯然,在 payoff() 方法中去插入 if else 語句是不 OK 的。這時,按照領域驅動設計的思想,應當將需求變更還原到領域模型中進行分析,進而根據領域模型背后的真實世界進行變更。這是上一個版本的領域模型,現(xiàn)在我們要在這個模型的基礎上增加折扣功能,并且還要分為限時折扣、限量折扣、某類商品的折扣等不同類型。這時,我們應當怎么分析設計呢?付款與折扣是什么關系呢?你可能會認為折扣是在付款的過程中進行的折扣,因此就應當將折扣寫到付款中。這樣思考對嗎?我們應當基于什么樣的思想與原則來設計呢?這時,另外一個重量級的設計原則應該出場了,那就是“單一職責原則”。單一職責原則:軟件系統(tǒng)中的每個元素只完成自己職責范圍內的事,而將其他的事交給別人去做,我只是去調用。單一職責原則是軟件設計中一個非常重要的原則,但如何正確地理解它成為一個非常關鍵的問題。在這句話中,準確理解的關鍵就在于“職責”二字,即自己職責的范圍到底在哪里。以往,我們錯誤地理解這個“職責”就是做某一個事,與這個事情相關的所有事情都是它的職責,正因為這個錯誤的理解,帶來了許多錯誤的設計,而將折扣寫到付款功能中。那么,怎樣才是對“職責”正確的理解呢?“一個職責就是軟件變化的一個原因”是著名的軟件大師 Bob 大叔在他的《敏捷軟件開發(fā):原則、模式與實踐》中的表述。但這個表述過于精簡,很難深刻地理解其中的內涵。這里我好好解讀一下這句話。先思考一下什么是高質量的代碼?你可能立即會想到“低耦合、高內聚”,以及各種設計原則,但這些評價標準都太“虛”。最直接、最落地的評價標準就是,當用戶提出一個需求變更時,為了實現(xiàn)這個變更而修改軟件的成本越低,那么軟件的設計質量就越高。當來了一個需求變更時,怎樣才能讓修改軟件的成本降低呢?如果為了實現(xiàn)這個需求,需要修改 3 個模塊的代碼,完后這 3 個模塊都需要測試,其維護成本必然是“高”。那么怎樣才能降到最低呢?如果只需要修改 1 個模塊就可以實現(xiàn)這個需求,維護成本就要低很多了。那么,怎樣才能在每次變更的時候都只修改一個模塊就能實現(xiàn)新需求呢?那就需要我們在平時就不斷地整理代碼,將那些因同一個原因而變更的代碼都放在一起,而將因不同原因而變更的代碼分開放,放在不同的模塊、不同的類中。這樣,當因為這個原因而需要修改代碼時,需要修改的代碼都在這個模塊、這個類中,修改范圍就縮小了,維護成本降低了,修改代碼帶來的風險自然也降低了,設計質量也就提高了。總之,單一職責原則要求我們在維護軟件的過程中需要不斷地進行整理,將軟件變化同一個原因的代碼放在一起,將軟件變化不同原因的代碼分開放。按照這樣的設計原則,回到前面那個案例中,那么應當怎樣去分析“付款”與“折扣”之間的關系呢?只需要回答兩個問題:- 當“付款”發(fā)生變更時,“折扣”是不是一定要變?
- 當“折扣”發(fā)生變更時,“付款”是不是一定要變?
當這兩個問題的答案是否定時,就說明“付款”與“折扣”是軟件變化的兩個不同的原因,那么把它們放在一起,放在同一個類、同一個方法中,合適嗎?不合適,就應當將“折扣”從“付款”中提取出來,單獨放在一個類中。- 當“限時折扣”發(fā)生變更的時候,“限量折扣”是不是一定要變?
- 當“限量折扣”發(fā)生變更的時候,“某類商品的折扣”是不是一定要變?
最后發(fā)現(xiàn),不同類型的折扣也是軟件變化不同的原因。將它們放在同一個類、同一個方法中,合適嗎?通過以上分析,我們做出了如下設計:在該設計中,將折扣功能從付款功能中獨立出去,做出了一個接口,然后以此為基礎設計了各種類型的折扣實現(xiàn)類。這樣的設計,當付款功能發(fā)生變更時不會影響折扣,而折扣發(fā)生變更的時候不會影響付款。同樣,當“限時折扣”發(fā)生變更時只與“限時折扣”有關,“限量折扣”發(fā)生變更時也只與“限量折扣”有關,與其他折扣類型無關。變更的范圍縮小了,維護成本就降低了,設計質量提高了。這樣的設計就是“單一職責原則”的真諦。接著,在這個版本的領域模型的基礎上進行程序設計,在設計時還可以加入一些設計模式的內容,因此我們進行了如下的設計:顯然,在該設計中加入了“策略模式”的內容,將折扣功能做成了一個折扣策略接口與各種折扣策略的實現(xiàn)類。當哪個折扣類型發(fā)生變更時就修改哪個折扣策略實現(xiàn)類;當要增加新的類型的折扣時就再寫一個折扣策略實現(xiàn)類,設計質量得到了提高。在第一次變更的基礎上,很快迎來了第二次變更,這次是要增加 VIP 會員,業(yè)務需求如下。- 對不同類型的 VIP 會員(金卡會員、銀卡會員)進行不同的折扣;
- 在支付時,為 VIP 會員發(fā)放福利(積分、返券等);
我們拿到這樣的需求又應當怎樣設計呢?同樣,先回到領域模型,分析“用戶”與“VIP 會員”的關系,“付款”與“VIP 會員”的關系。在分析的時候,還是回答那兩個問題:- “用戶”發(fā)生變更時,“VIP 會員”是否要變;
- “VIP 會員”發(fā)生變更時,“用戶”是否要變。
通過分析發(fā)現(xiàn),“用戶”與“VIP 會員”是兩個完全不同的事物。- “VIP 會員”要做的是會員折扣、會員福利與會員特權;
- 而“付款”與“VIP 會員”的關系是在付款的過程中去調用會員折扣、會員福利與會員特權。
有了這些領域模型的變更,然后就可以以此作為基礎,指導后面程序代碼的變更了。同樣,第三次變更是增加更多的支付方式,我們在領域模型中分析“付款”與“支付方式”之間的關系,發(fā)現(xiàn)它們也是軟件變化不同的原因。因此,我們果斷做出了這樣的設計:而在設計實現(xiàn)時,因為要與各個第三方的支付系統(tǒng)對接,也就是要與外部系統(tǒng)對接。為了使第三方的外部系統(tǒng)的變更對我們的影響最小化,在它們中間果斷加入了“適配器模式”,設計如下:通過加入適配器模式,訂單 Service 在進行支付時調用的不再是外部的支付接口,而是“支付方式”接口,與外部系統(tǒng)解耦。只要保證“支付方式”接口是穩(wěn)定的,那么訂單 Service 就是穩(wěn)定的。比如:- 當支付寶支付接口發(fā)生變更時,影響的只限于支付寶 Adapter;
- 當微信支付接口發(fā)生變更時,影響的只限于微信支付 Adapter;
- 當要增加一個新的支付方式時,只需要再寫一個新的 Adapter。
日后不論哪種變更,要修改的代碼范圍縮小了,維護成本自然降低了,代碼質量就提高了。軟件發(fā)展的規(guī)律就是逐步由簡單軟件向復雜軟件轉變。簡單軟件有簡單軟件的設計,復雜軟件有復雜軟件的設計。因此,當軟件由簡單軟件向復雜軟件轉變時,就需要通過兩頂帽子適時地對程序結構進行調整,再實現(xiàn)新需求,只有這樣才能保證軟件不退化。然而,在變更的時候,如何調整代碼以適應新的需求呢?
DDD 給了我們思路:在每次變更的時候,先回到領域模型,基于業(yè)務進行領域模型的變更。然后,再基于領域模型的變更,指導程序的變更。這樣,不論經歷多少次需求變更,始終能夠保持設計質量不退化。這樣的設計,才能保障系統(tǒng)始終在低成本的狀態(tài)下,可持續(xù)地不斷維護下去。
IDCF DevOps黑客馬拉松,獨創(chuàng)端到端DevOps體驗,精益創(chuàng)業(yè)+敏捷開發(fā)+DevOps流水線的完美結合,2021年僅有的3場公開課,數(shù)千人參與并一致五星推薦的金牌訓練營,追求卓越的你一定不能錯過!7月30-31日,北京站,還有少量參賽名額。一年等一回,錯過等一年,趕緊報名吧??