January 04 00:00~01:10
前言
今年一月上班沒幾天就要過農曆年,去年底在安排泰迪軟體2023年課程表的時候,索性就把整個一月空下來。本來想要找機會再溜出國玩,後來因為去年年底太忙,沒時間安排行程,乾脆把時間拿來在家裡讀書算了。
這幾天同時間讀了幾本書,其中有一本《Five Lines of Code: How and when to refactor》滿有意思的。這是一本討論Refactoring(重構)的書,有別於傳統的重構,書中採用Rule-Based Refactoring,藉由提供幾條非常具體的規則,讓開發人員知道何時以及如何重構。例如書中的第一條規則就是這本書的書名:每一個method的程式碼不可以超過5行(不包含括號)。
今天Teddy要談這本書提到的另一個重構中經常遇到的問題,就是Duplicated Code(重複程式碼)。傳統上認為重複程式碼是Bad Smell(壞味道、怪味道),應該要將其斬草除根。關於這一點,基本上沒什麼爭議。如果程式中有很多重複程式碼,一旦需求改變涉及到這些程式碼,開發人員就需要修改每一處的重複程式碼,否則系統就會發生錯誤。這就造成另一個壞味道:Shotgun Surgery。
***
再探重複程式碼
這幾年Teddy因為學習Clean Architecture與Microservices,發現透過重複程式碼來避免模組之間的依賴,反倒是一種很常見的方法。例如,Clean Architecture的跨層原則要求Entities Layer的物件離開Use Cases Layer時必須轉成另外一種資料結構;若是往前端傳遞則轉成DTO(Data Transfer Object),往資料庫傳遞則轉成PO(Persistent Object)。雖然DTO與PO與其所相對應的Entities Layer物件並不一模一樣,重複的部分只有資料,但廣義來看這也可以視為一種重複程式碼。
《Five Lines of Code: How and when to refactor》書中對這一點解釋的很好:
Sharing code increases global behavior-change velocity, while duplicating code increases local behavior-change velocity.
針對某個功能,如果全部程式碼都共用(沒有重複程式碼),那麼當系統行為改變的時候,只要改一個地方即可,因此「global behavior-change(全域行為改變)」的速度會很快。反之,如果同一個功能每次用到它的時候都將其複製一份,那麼這份複製出來的程式碼就與原本的「本尊」獨立,去除了耦合。在這種情況下,「local behavior-change(區域行為改變)」的速度就會很快。
從生物學的角度來看,獨立的區域,例如島嶼、沙漠或高山,經常會演化出「特有種」生物。一開始這些生物的起源是相同的,但是為了應付「區域性需求」,逐漸演化出不同的特徵。 程式碼也經常如此,因為被共用的程式碼在使用它的地方可能同時存在不同的區域性需求(書中稱為local invariants),此時如果堅持「共用」,很可能導致需要回頭修改共用程式碼,讓它具備適應不同區域性需求的能力(透過設定或依賴注入),因而增加共用程式碼的複雜度,最後可能會複雜到降低它的可讀性,因而反倒造成可修改性下降(原本共用希望提升global behavior-change速度,但在這種情況下,可能反倒降低global behavior-change速度)。
***
Shotgun Surgery的問題勒?
討論至此,Teddy也不是鼓吹盲目使用「複製、貼上」來去除模組之間的依賴這樣就好棒棒,「利用重複性去除依賴」這個方法,還是要從軟體架構與邊界這兩個角度來考慮。假設你發生重複性的邊界是在同一個method裡面,這種重複性幾乎可以斷定是壞味道,可以用Extract Method將其移除。如果發生重複性的邊界是同一個package,則也有很大的機會是壞味道。但是如Teddy前述的情況,在Clean Architecture的跨層原則之下,重複性的邊界已經是架構階層之間,此時使用重複性去除依賴爭議就比較小。
另一個常見的情況是在微服務架構中,下游微服務透過聽取上游微服務的事件,在本地端建立讀取模型(Read Model)以隔離兩個微服務之間的執行期間依賴。
***
結論
Teddy在開發ezKanban的過程中也遇到很多「是否要共用,還是利用重複性去除依賴」的設計決定,例如ezKanban支援Event Sourcing與State Sourcing這兩種狀態儲存方式,一開始ezKanban的報表先支援State Sourcing,然後再支援Event Sourcing。為了支援這兩種儲存方式,團隊在Repository的實作中套用Pluggable Adapter設計模式,整個設計簡單易懂。
等開發完成之後,團隊發現有不少報表除了撈資料的方式不同(一個下SQL另一個操作event streams),其餘產生報表的計算邏輯大致上是相同的。為了去除這些重複性,又花了不少時間重構系統。重構完成後,去除重複程式碼,但付出的代價就是設計變得更間接(因為多了一層抽象介面),也沒那麼直覺。
Teddy覺得這是一個有趣的議題,重新省視「程式碼共用」這件事,共用並不一定都是好的,想想你的中台…XD。
***
友藏內心獨白:To share, or not to share, that is the question。
中心化和去中心化好像是分久必合合久必分的讨论。有一项原则值得关注就是不要高估人类的智力。保持软件简单很必要。如果除去了类似代码导致了在智力上理解的难度其实很得不偿失。而天天泡在熟悉项目里的人很难察觉到这差距。是否触发合并删除也有个数字3吧。因为2如果少了一个那么剩下的一个独立中心化的东西会非常尴尬
回覆刪除