1. 程式人生 > >架構漫談(八):從架構的角度看如何寫好程式碼

架構漫談(八):從架構的角度看如何寫好程式碼

架構漫談是由資深架構師王概凱Kevin執筆的系列專欄,專欄將會以Kevin的架構經驗為基礎,逐步討論什麼是架構、怎樣做好架構、軟體架構如何落地、如何寫好程式等問題。

本文是漫談架構專欄的第八篇,作者Kevin舉例介紹瞭如何寫好程式碼。當我們有了好的架構,那就需要考慮如何將架構落地,而這個時候,程式碼就顯得無比重要了!千萬不要讓程式碼成為架構擴充套件的瓶頸。文中作者提到了程式碼架構,細細品味吧。

在第六篇文章中,我們得出一個結論,軟體架構實際上包括了:程式碼架構,以及承載程式碼執行的硬體部署架構。實際上,硬體部署架構最終還是由程式碼的架構來決定。因為程式碼架構不合理,是無法把一個執行單元分拆出多個來的,那麼硬體架構能分拆的就非常的有限,整個系統最終很難長的更大。

所以我們經常會聽說,重寫程式碼,推翻原有架構,重新設計等等說法,來說明架構的進化。這實際上就是當初為了完成任務,沒有充分思考所帶來的後果。這也並不是架構進化的事情,而是個人對問題領域的逐漸深入理解的過程。所以有必要再討論一下,程式碼的架構應該是怎樣的。

本文會在之前幾篇文章的基礎上,進一步探討如何把架構的思考進行落地,細化到我們程式碼的實踐當中,儘量不要讓程式碼成為系統長大的瓶頸,降低架構分拆的成本。

在前面我們提到,軟體實際上是對現實生活的模擬,虛擬化。這是一個非常重要的前提,直接決定了我們的程式碼應該分為幾部分。結合每個部署單元所承擔的責任,可以明確的拆分為兩個不同的責任:

  1. 表達業務邏輯的程式碼。很多人把這部分叫做Domain Logic,或者叫Domain Model。這部分實際是來源於生活的,必須保持和現實生活中的切分一致,並非人為的抽象而成。

  2. 對使用者提供訪問並儲存業務邏輯執行結果的程式碼。計算機的狀態儲存有一個缺陷,本機保留業務執行結果有很大的問題,一般都在外儲存裝置上儲存,也便於擴充套件。

所以單個部署單元的程式碼可以分為兩個部分,如下圖所示:

(點選放大影象)

從這個圖中可以看出,軟體程式碼的相關利益人為執行時的訪問人員和儲存裝置。而service的程式碼是最複雜的,需要服務於三方,程式碼人員的負擔是最重的。為了把這三方的變化對service的影響降到最低,對於service還必須進一步的分拆為三個部分,讓每一個部分都能夠獨立的變化,這樣這三方的變化就不會產生連鎖響應,降低成本。如下圖所示:

(點選放大影象)

這樣,就劃分成了幾個責任:

  1. Service就專注於user的需求,並組合Glue Code提供的服務完成需求。

  2. Glue Code專注於組合business的呼叫,管理Business裡面物件的生命週期,並且通過Repository儲存或載入Business的狀態

  3. Business專注於實現業務的核心模型。

  4. Repository專注於資料的儲存,並和儲存裝置一一對應。

大家注意看,還是樹形架構。並且左側的主要需要計算機的相關理論知識,並且要直接面對使用者的需求。右側的更多的需要面對業務的核心。只要這幾塊的開發人員互相商量好了介面定義,這幾個部分的開發就可以並行的進行,極大的提升開發的效率,縮短開發的時間。要做好這幾部分,還需要注意,邏輯只允許存在於Business中,Service、Glue Code、Repository都不允許存在業務邏輯。為什麼呢?首先我們來看看什麼叫業務邏輯。

什麼叫業務邏輯?

首先這個定義的前提是指軟體程式碼中的邏輯,不是現實生活中的邏輯。在軟體程式碼中,不需縮排和計算的順序呼叫,包括縮排的程式碼目的是catch exception的,都不算邏輯,除此以外都是邏輯。以下用嚴格的順序呼叫來指代這種程式碼。因為順序呼叫是計算機的特性,由編譯器來決定的,當然最本質的是因為我們計算的基礎都是圖靈機。在現實生活中,順序呼叫也是邏輯,大家不要和我們這裡說的業務邏輯相混淆。

為什麼說除了Business程式碼中有邏輯以外,其他地方不能有邏輯呢? 我們每個部分分別分析:

  1. 如果service裡面不是嚴格的順序呼叫,有很多分支,那麼說明這個service做了兩件或者兩件以上的事情。必須把這個service分拆,確保每個service只做一件事情。因為如果不這麼分拆的話,一旦這個service中的某各部分發生變動,其他的部分的執行必定會受影響。而確定到底有哪些影響的溝通成本非常高,其他相關利益方沒有動力去配合,我們往往不會投入精力仔細評估。最後上線會出很多不可預料的問題,最終會導致損失使用者的利益,並且肯定會導致返工,損壞自己的利益。如果是有計算的邏輯的話,比如受益計算,訂單金額計算等,那麼這部分應該是Business程式碼需要完成的,不能交給service程式碼來實現。

  2. Glue Code裡面如果不是嚴格的順序呼叫,同理會和service一樣遇到同樣的問題。

  3. Repository裡面如果不是嚴格的順序呼叫,包括儲存訪問的程式碼裡面(比如SQL),會導致邏輯進入到儲存裝置中。儲存裝置的主要目的是拿來儲存的,一旦變成了邏輯計算的主體,就會導致儲存裝置無法通過增加機器的方式橫向擴充套件長大。這個時候就沒有架構了,只能換效能更好的機器,這個叫scale up。只有scale out才能算架構。

以上都會導致架構無法快速的橫向擴充套件和分拆,並且增加了修改的成本,這些是不符合開發人員以及業務的利益的。

這麼做的好處有哪些呢?

  1. Service、Glue Code、Repository裡面的程式碼是嚴格的順序呼叫,那麼這些程式碼只要做連通性測試即可,不需要單元測試。因為這些程式碼都需要和很多上下文打交道,很難做單元測試。這樣才算是真正的組合。

  2. Business不訪問任何上下文,不訪問任何具體的裝置,所以這部分程式碼是非常容易些單元測試的,並且單元測試必須100%覆蓋。因為其他地方沒有業務邏輯,所以一旦有問題,就可以斷定是Model的問題,單元測試肯定可以發現。如果單元測試沒有發現問題,那麼單元測試一定有問題。線上問題的模擬也就變得非常的簡單,單元測試也能夠得到進一步的補充。

  3. Repository很容易按照儲存裝置本身的最小訪問粒度來完成工作,比如DB,完全可以做到單表訪問。因為這個時候儲存裝置只關心存取資料,完全和業務沒有關係。做表的分拆也是非常容易的事情,儲存裝置通過增加機器就可以橫向擴充套件長大。很多人會擔心說,沒有了join,訪問DB的次數是不是更多了,會導致效能下降? 按照現在網路的條件,網路訪問和Disk IO訪問的差距已經不大了,合理的設計下,多訪問幾次DB並不會導致這個問題。另外如果多臺DB的話,還能通過並行加速訪問。

  4. 由於Service、Glue Code、Repository程式碼簡單了,才可以讓我們的開發人員投入更多的時間研究業務,畢竟這部分才是軟體所真正服務的物件。

我們再來看一個實際的例子,如下圖所示:

(點選放大影象)

Manager類實際就是Glue Code。有幾個注意點需要說明一下:

  1. 不能把Business Model當做資料物件來處理,Model關心的實際上是業務行為,資料只是是這些行為的結果。所以Glue Code需要把Model轉換為Entity,Entity和儲存裝置裡面的儲存粒度一一對應。比如在DB中,每個Entity對應一張表,並且跟著表的變化而變化,這樣就保證儲存的變更不會影響Model。同樣Service和使用者之間的資料互動,也是不會和Model之間相關的,確保使用者的需求變化,不會影響到Model。因為使用者的需求變化是最頻繁的,沒有邏輯,可以讓我快速的滿足業務的需求。

  2. 在Service這裡,最好不要考慮程式碼重用。因為當多個不同的角色訪問同一個介面,一旦某個角色的需求發生了變化,就會要求開發人員去修改。而這個修改往往會影響到其他的角色,需要這些角色一起配合來確定是否受影響,但是這些角色因為沒有需求,往往不會配合。這樣就給開發人員造成了很多不必要的溝通,成本是非常高的。最終都會導致線上Bug,影響最終的使用者。所以儘量給不同的角色不同的Service,避免重用,降低溝通成本。很多人會說這樣Service不就太多了嗎? 這樣Service註冊,查詢等管理需求就出現了,Service治理中心就是來解決這個問題的。因為Service裡面沒有邏輯,所以開發和管理非常的簡單,可以快速應對業務的變化。我們只有更快地變,更容易的變,才能更好地應對變。

  3. Business Model是必須要重用的,一旦發現重用出現問題,那麼說明Business Model的識別出現了問題,這是一個我們要重新思考Model的訊號。Business Model必須是一個完美的樹狀,如果不是,也說明Model的識別出了問題。

在實際操作中,Service、Glue Code、Repository不能有邏輯,實際上和很多人的觀念是衝突的,認為這個根本做不到。做到這一點需要很多的學習成本,但是一定可以做得到。當發現做不到的時候,可以斷定是業務的分析出了問題。比如不該合併的合併了,不該計算的計算了。這個問題一定有辦法解決的,做不到都是理由,無非是想早點把自己的工作結束罷了。雖然剛開始會比較困難,一旦把這個觀念變成自覺,開發的質量和效率馬上就能高好幾個級別。

我的游泳教練曾和我說過這些話,我至今記憶猶新:“業餘選手,越想從水裡浮起來,就越想把頭擡起來,身體反而沉下去。只有克服恐懼,把頭往水裡壓下去,身體才能夠從水裡浮起來。真正專業的習慣往往是和我們日常的行為相反的”。

我們真正想快速的完成程式碼工作,就要克服自己對時間的恐懼,真正的去研究業務的問題,相關stakeholder的利益,把這個變成我們的習慣。寫程式碼的時候讓該出現邏輯的地方出現邏輯,讓不該出現的地方不能出現。一旦不該出現的地方出現了邏輯,那麼要馬上意識到,這個地方是一個坑,這個問題一定和業務的分析不透徹有關係。

很多人可能會把這個做法和Martin Fowler曾經提出過充血模型和貧血模型來比較,和Domain Driven Design來比較,其實沒有必要。這個分拆完全是從軟體所解決的問題,根據軟體架構推匯出來的,很多地方和兩位前輩的觀點是一致的,但是並不完全等同。

以上只是針對單一的Service部署單元的分析,擴充套件開去,對於其他的部署單元也是類似的。每個單元的下一級都可以認為是Repository,每個單元的上一級都可以認為是User。這些實踐在我自己的專案中都有用到,非常的有效,迭代的速度非常的快。很多人擔心Business Model建不好,其實沒關係,剛開始可以粗糙一點,後續可以慢慢的完善。這個架構已經隔離好了每個部分的變化對其他部分的影響,變化成本都在可控的範圍之內。

轉自:http://www.infoq.com/cn/articles/an-informal-discussion-on-architecture-part08