更多文章請參見:我的blog
0. 背景
本文主要討論的內容是代碼層面的內容。之前將軟件行業的認知分為七個層次,這里主要的內容集中在代碼層面,會涉及一些程序、軟件層次中的內容。適合與剛入行的同學閱讀。為初級工程師快速的成為中級工程師提供一條道路。
本文從幾個常見的問題入手開始討論寫代碼過程中可能遇到的問題(第一節),再討論編寫代碼中除了常見問題還應該考慮哪些問題(第二節),討論完遇到的問題以及要考慮的問題之后開始開始討論哪些方式可以解決這些問題(第三節),到最后以實踐的角度落地這些解決問題的方法(第四節)。以深入淺出的方式說明寫出好代碼并不是那么的難。
1. 代碼為什么越寫越亂?
在新寫的代碼和老代碼的改造/重構過程中,總會發現因為技術、業務將一個簡單的問題弄的越來越復雜。從而導致代碼越來越難看,越來越不能被理解。那么有哪些原因導致代碼越來越混亂?
1.1 結構化程序設計的主要特點流程式代碼
從學習過程來看,任何一個研發人員都是先從面向過程編程開始學習變成的。在學習的過程中一個最主要的過程就是使用順序的方式將所要實現的內容實現了。
在軟件工程中我們學習過軟件設計中需要包含模塊/組件,但是很多軟件從業者沒有辦法將這個概念落地到代碼編寫過程中。因為沒有辦法劃分模塊的邊界、確定模塊的意義、描述清楚模塊間關系,所以就不進行模塊的拆分。很多從業者不能理解模塊/組件到底應該落在軟件認知7層模型中的那一層也是影響開發人員不進行模塊劃分的原因。
1.1.1 流程化的代碼編寫方式有問題嗎?
經常遇到一個文件打天下(上帝類)的情況,例如在mvc下的service是不是上帝類,該做的不該做的都放在這個里面?
一個Method走天下,還是例如在mvc中一個業務都直接寫在service的一個方法中。一個方法一般情況下都在200行以上,這種就很容易形成鐵桶一塊的問題。根本沒有辦法修改這個方法,如果要新添功能就直接在方法中加一塊代碼。
1.2 MVC是萬能的?
在WEB編程興起的年代,也伴隨著MVC模式的興起。所以,大家在學習WEB編程時都在學習和使用MVC模式。而MVC的特點是它由Model,View,Controller組成。而在微服務時代View都由前端實現,后端基本不用操心這方面的內容。Controller中主要做轉換,控制,安全。那么業務邏輯應該在哪里完成呢?在Model中嗎?這就是MVC沒有辦法解決的問題。
1.2.1 面向對象
在MVC中實際實現過程中,Model中由Entity、Service和Dao組成。Entity負責業務實體,Service負責業務處理,Dao負責持久化。而Service中寫的代碼都是以面向過程的方式進行編寫的。所以Service和Model的概念都是沖突的。Model代表的是業務模型,Service根本就沒有辦法代表業務模型。也沒有辦法說用Java寫的代碼就是面向對象的。
1.2.2 代碼的邊界在哪里?
上面說到的一個文件打天下,一個方法打天下是很明顯的代碼的邊界是業務流程。并不用吃驚,因為很多同事嘴上說著做技術,其實寫代碼的時候都是寫的業務流程代碼。
這里只想說這種代碼邊界是錯誤的。原因有這么幾點:
- 業務流程的公用性比較小,所以導致方法,文件的公用性更小。從而導致代碼無法復用。
- 業務流程沒有拆分,無步驟則代碼不易讀。造成很難維護。
1.2.3 分層關系
在MVC的優點中做了一項叫做開閉原則:對擴展開放,對修改關閉。做了不同層次之間的封裝,做了一層隔離。
1.3 軟件復雜度的三個來源:規模,結構與變化
《解構領域驅動設計》中張逸老師說到軟件復雜度的來源:規模,結構與變化。這三項最終都會落在代碼中,例如業務會不斷的發展,不斷的增長,所以代碼的規模也會不斷的增長。稍微上規模一點的業務系統,都會牽扯到各種各樣的實體,以及實體之間的關系導致結構的復雜度提升。業務是否會演進,業務演進就會帶動代碼的變化。
在代碼層次中怎么應對這些內容呢?
1.4 總結
從結構化變成深入到軟件的復雜度,一路上都是各種問題來影響代碼的編寫過程。考慮每一個方向都有可能和其他方向的代碼有質的區別,我們這里并不討論這幾個方向深入之后會產生什么樣的代碼,我們這里討論公共的一些好代碼的寫法。
1.4.1 問題
大家都習慣了流程化的代碼編寫方式,并深受MVC之毒(其實并非MVC問題,而是不深入思考)。在這種前提下又有這么復雜的問題需要編寫代碼來解決。代碼寫的爛是不是變成了正常事。
1.4.2 解決問題
-
分治思維
結構化程序設計的主要特點是拋棄 goto 語句,采取“自頂向下、逐步細化、模塊化”的指導思想。
結構化程序設計本質上還是一種面向過程的設計思想,但通過“自頂向下、逐步細化、 模塊化”的方法,將軟件的復雜度控制在一定范圍內,從而從整體上降低了軟件開發的復雜度。--李運華《從零開始學架構》
-
不斷的實踐設計模式
學會寫OO代碼,并能夠理解KISS,DRY,SOLID,最少知識、向穩定依賴原則。并能夠在工作過程中不斷的實踐這些原則。
下面主要說明落地這兩個方向會遇到怎樣的問題?以及怎樣解決這些問題?
2. 怎么做到高內聚低耦合?
現代軟件的代碼第一目標是可讀性,其他的事情可靠,性能,安全等都是可以通過其他的方式解決的。所以,在編寫代碼的過程中第一要務是讓代碼能被別人看懂。
如下圖隨心所欲的做事和有規則的做事,有很大的區別。區別就在于怎么進行分類整理?
下面逐步深入討論一下代碼怎么做到高內聚低耦合。第一步討論劃分代碼模塊(函數)時可能遇到的問題,第二部討論劃分模塊(函數)時是不是應該考慮所有原則,最后以統一的方式進行解答并引申到下一節的結構化。
2.1 模塊/組件的定義
在編寫代碼過程中并不是只寫方法(函數)就可以,還需要進行文件劃分,Package劃分。這些在某種意義上就是不同層次的模塊劃分。但是劃分過程中可能會遇到這段代碼應該放在文件A中還是文件B中的問題,這里會從不同的層面列出問題,讓大家對這部分有更深入的思考。
2.1.1 定義
模塊的責任怎樣確定
負責模塊中的事務嗎?
負責模塊中的持久化動作?
模塊中的業務怎么確定應該在模塊中還是在模塊外?模塊之間的關系是什么樣的?
如果兩個模塊有關系,他們是不是直接進行調用?
模塊是不是落在不同的層次中?層次之間的依賴關系是不是服務的關系?層次是洋蔥架構層次還是上下分層?
使用函數是編程中的處理類傳遞還是使用DI的方式進行依賴傳遞?模塊中任何地方都可以調用其他的服務嗎?
對外能力(方法、功能)怎么確定?
內部能力(方法、功能)可以被任何其他模塊調用嗎?
2.1.2 工程代碼與算法代碼的區別
算法代碼代表著一個功能
所有的代碼都需要寫在一起,因為必須在當前位置調整指針位置,調整當前值內容等。工程代碼是分步驟的
一個業務寫一個流程就可以解決所有的業務問題還是按照規則去完成高內聚低耦合?
領域?微服務?限界上下文?領域?OO(面向對象)
工程代碼是給人看的,所以第一要務是讓人能看得懂
2.2 原則
除了SOLID之外還有KISS,DRY,BASE,約定大于配置,奧卡姆剃刀等等原則。那么在哪里使用這些原則,有沒有反模式?
2.2.1 寫代碼的時候是面向復用編程,還是面向業務編程?
很多代碼編寫的過程都是BA/PM輸出需求,然后開發進行代碼編寫。那么開發順著需求的思路進行實現過程中是不是就變成了按照業務進行編程,再結合之前的問題一個Method打天下。就變成了業務流程寫在一個方法中。那么怎么面向復用編程?
2.2.2 最少知識
接口規則怎么影響高內聚與低耦合?數據庫中的2NF(部分子函數依賴)是不是會影響接口的定義?
內部實現的內容不應該通過參數被暴露出來?
2.2.3 單一職責
舉一個簡單的例子:
- 在業務的參數校驗中能不能進行服務間調用完成業務?
- 上面說到工程代碼是分步驟的,步驟之間的責任是否定義清晰?
- 在Controller的Method中進行業務編寫是否合適?Controller中應該干什么?
2.2.4 圈復雜度
圈復雜度大說明程序代碼的判斷邏輯復雜,可能質量低,且難于測試和維護。復雜度越高代表讀懂代碼越難,其他人讀懂代碼代表著是不是可以維護。不過圈復雜度只能代表代碼層次的可讀性,不能代表程序層次的可理解性。
這里說明最簡單的降低代碼圈復雜度的方法:不要if中嵌套if,不要循環中嵌套循環。下面解決方案中會有更加完善的解決辦法。
2.3 拆分+明確解決問題
從原來的雜亂無序,到結構化定義。其實就是使用拆分+明確邊界的方式進行解決。拆分和明確可以利用大規則:抽象、分解和知識來進行。最需要的就是以這種方式進行思考,將這種思考模式應用到軟件的各個層次。
2.3.1 實踐
空行的意義
一個方法一個函數中,空行的意義是隔開不同步驟之間的內容。在一些不用拆的很開的代碼塊之間有需要說明他們是不同的意義的代碼塊之間用空行隔開。方法名與領域命令之間的關系
面向對象課程中教過方法就是對象的行為。那么行為是鴨子叫?還是你打了鴨子一下鴨子追著你叫?所以這里應該是需要了解對象是需要處理什么樣的動作,然后動作中需要有什么處理流程。-
方法的階段性劃分
代碼編寫范式就是設計模式,但是設計模式中沒有說明一個業務代碼應該怎樣拆分步驟。這里給出一個作者認為通用的方法階段拆分流程。- 準備參數
將參數校驗中需要的數據準備好。 - 參數校驗
進行參數的校驗動作,例如在修改動作中查找原對象是否存在,對象的字段是否符合業務意義。 - 業務步驟
業務動作,業務動作可能是多個步驟。例如在電商中購物車生成訂單的業務步驟獲取商品信息,暫存商品信息,生成訂單,生成訂單項,通知店家等等。 - 返回結果
返回處理結果
- 準備參數
3. 代碼怎么寫才能不亂?
上面提了那么多問題,這里就開始說明怎么解決這些問題。其實治理一件事情很簡單就需要處理三件事情即可:
-
明確事務邊界
下面以分包模式的說明進行討論。 -
明確事務間的關系
下面以金字塔原理的方式解決。 -
明確事務演進的方向即可
這個其實在代碼這個層面上比較少,所以就不進行說明了。
3.1 分包模式
軟件工程的發展過程其實就是不斷的明確包(組件)的職責與劃分方法的發展史。清晰架構是到現在為止作者看到最新的一代分包模式,如下圖所示。
[圖片上傳失敗...(image-6d64ae-1659096072391)]](https://upload-images.jianshu.io/upload_images/2454595-f05c26388a7d99b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/740)
這里的內容不進行詳細討論,如有興趣可以自行檢索。這里就說明通過將業務、業務與技術鏈接、技術之間通過模塊定義以及依賴關系定義的方式拆分開。以領域模型管理業務中的復雜度落在代碼就可以保證領域中的代碼的單一與簡單,以各種組件將業務與技術關聯、技術代碼拆離到不同的組件中。
3.2 金字塔原理
結合清晰架構對于層次與模塊的劃分。
-
結論先行
應用層負責聚合領域服務來完成應用的業務,在編寫這層代碼時可以以結論先行的方式進行。即應用層只進行整體業務流程的編排工作,具體的業務操作在領域服務中完成。
這樣基本上看了應用層的代碼就可以知道這部分業務的整體業務流程是什么樣的。 -
中心思想明確
以領域模型的方式進行中心思想明確,每一個領域模型都有它自己的要處理的業務,并且不牽扯到其他的業務。以領域模型能力暴露的方式控制了模塊的邊界。 -
先全局后細節
以不同的層次的職責與依賴關系來管理細節的遞進關系。通過不斷的細化,讓上帝類消亡。
4. 組織模塊的方式?
應對軟件復雜度的方式最有名的就是DDD,但DDD并沒有實際的代碼編寫方式的指導工作。而Cola是DDD的一種比較全面的代碼落地框架。
充血模型的問題可以參見DDD 中的幾個困難問題。所以這里沒有以充血模型進行。根據作者淺薄的理解對拆包進行了些許的變化,如有任何問題可以聯系作者。
.
├── cola-archetype #cola核心部分
│ ├── config #配置管理
│ ├── common #公共部分
│ ├── adapter #接入層
│ │ ├── controller #http接口
│ │ ├── rpc #rpc接口
│ │ └── amqp #消息隊列消息入口
│ ├── connector #外部代用
│ │ ├── sms #短信接口
│ │ ├── email #短信接口
│ │ ├── amqp #消息隊列消息出口
│ │ └── db #數據庫
│ ├── app #應用層
│ │ ├── AAA應用能力 #負責CQRS和Event的事項處理,以及應用業務流程的整體控制。
│ │ └── BBB應用能力 #負責CQRS和Event的事項處理,以及應用業務流程的整體控制。
│ ├── domain-service #領域服務層
│ │ ├── XXX領域服務 #負責一個領域中的對外能力的暴露。
│ │ └── YYY領域服務 #負責一個領域中的對外能力的暴露。
│ └── domain #領域層
│ ├── XXX領域 #XXX的領域分包
│ │ ├── entity #負責這個領域中的實體定義。
│ │ ├── event #負責這個領域中的事件定義。
│ │ ├── handler #負責這個領域關心的事件的處理。
│ │ ├── service #負責這個領域中的能力的提供。
│ │ └── XXXAggregateRoot[.java/.go/.python/...] #領域聚合根,可能和領域服務層有些沖突
│ └── YYY領域 #YYY的領域分包
└── cola-components #cola組件部分
├── dto #dto
├── exception #exception
├── extension #extension
└── test #test
5. 總結
《重構》中有非常完善的代碼好的樣子和壞味道的樣子。本文主要討論的是代碼管理以及代碼編寫中的內容。總結就是代碼需要以先全局后細節的方式進行編寫。
6. 參考
清晰架構
金字塔工作法
重構
圈復雜度
DDD 中的幾個困難問題