單一職責
我對單一職責(SRP)的啟發式理解是「能單測,沒有副作用,一個函式只做一件事」,當然我知道這不完全嚴謹。但作為快速判斷我覺得是好的。
能單測。意味著這個函式不依賴外部狀態,沒有額外的規則,在純粹的情況下就可以測試,往往也暗示了此函式已經分離到了相當的程度。
沒有副作用。這點不總是絕對的,比如有些函式的合法職責就是帶副作用的,像打log或者查資料庫。核心點在於——除非這真的是這個函式的合法職責,否則它不應該影響到本該由其他函式做的事情,此時採用委派是最好的。即,委派給一個專職做此時的函式,而不是自己親自去進行。
只做一件事。這裡的一件事,本身不能從字面上理解,而是要考慮抽象的層次。比如在更高的抽象層次,這裡的「一件事」能涵蓋很多,不如說是一種「職責」。這裡的「職責」本來就是蠻抽象的東西,我對其的理解是「職能」。此函式在某一地方發揮的最小的單一功能。
更明確的SRP訊號其實是「改變」。比如當未來我進行修改的時候,是不是隻會因為一個「原因」去改變他?或者更確切的說,我該這個函式的時候,不會改到不相干的東西,比如改一個查資料庫的函式還得估計別把http請求改壞了——拿著就是職責混同。正是因為混了不同的東西,才會因為不同原因來這裡改這個函式。而反過來,這種函式也是最難單測試的,因為為了測試它,你得構建不同的環境。所以「按變化原因切分程式碼」是更強烈的SRP訊號,關鍵在於維護的邊界。
還有一種函式的職責是「編排」。對這種函式來說,它就像最終的執行者一樣,把好幾個不同職責的函式打包在一起執行。要注意,業務邏輯和編排是不一樣的。編排只是按照特定的順序執行,但業務邏輯則會承擔諸多狀態控制和判斷分支,對於後者來說,這完全就是職責不清晰。雖然同樣是委派,但如果摻雜了單純執行之外的其他因素,就可能變成一種包裝——本質上只是把業務邏輯扔了出去而已,實際上還是耦合的。
關鍵在於「狀態」。如果你的狀態本身是別人的,那麼這一部分的職責最好就是回到它發生的地方。如果一段程式碼主要都是在處理別人的狀態,就說明它該拆分和重構到其狀態所屬之處了——這意味著當前的程式碼是耦合的,耦合與他處的狀態。「依戀情結」正是這樣一種壞味道,全在處理別人家的狀態。如果對方只有一個,那這一整部分應該移到那邊;如果有多個,這裡就分兩種情況:1.多個一起變,而且是為了一個目的變,每次都固定搭配,而且缺一不可,那就說明這一部分是一個單獨的職責,應該給這個單獨給一個方法或者物件,2.多個分開變,那就得拆出來,作為單獨的方法存在;以及純粹的排序,比如固定的生命週期,而不是彼此搭配的狀態操作,那就是我們上面說的編排。
區分在於職責,如果某個職責本身已經有人——或者現在還沒人——那就應該由它去做。而不是由職責不同的另一個方法去做。重點是分辨三個:委託、依戀和編排。這裡有個地方很難搞清楚,那就是「自己的狀態」——到底是啥?這裡的核心就是,那些固定搭配,反覆操作的狀態,就是「自己」,這個「自己」在我們給他封裝之前可能是不存在的,但一旦包裝好,就可以統一把這謝謝狀態給他自己管理了,此時它再修改的就是自己的狀態,自己負責。當我不是單純在這裡進行一次的操作(這是委派)而是每次都在做一個流程(一組操作)的時候,這說明我可能越界了。