SOLID React:在 React 中應用 SOLID 原則

fin
嗨,世界
Published in
Oct 18, 2020

當專案變大、變複雜,若是不好好處理,維護成本或是開發成本都會逐漸提升,最後到一種無法動彈的地步。系統演變至此,(看起來)最低成本的方式就是砍掉重練了。

年輕人終究是年輕人 https://memes.tw/gif-maker

實際建構過幾個專案後一定會碰到這問題,於是我們會想要讓程式碼更好維護,這時就需要 SOLID 原則的幫忙。系列文預計依序介紹 SOLID 的五個原則,來看看這五個原則將如何應用在 React 專案內,本文將從第一個原則開始:

單一責任原則 SRP Single Responsibility Principle

A class should have one, and only one, reason to change.

在跳進 React 範例之前,還是先來回顧一下單一責任原則。這是一個相對容易理解的原則,套用到 React 專案就是,一個功能拆成一個元件(檔案)。但原則之簡單,其實相對的代表了實踐上的困難,因為真實世界不可能這麼簡單,要如何把一堆邏輯拆解成一個個遵照簡單原則執行的元件,這還真不是講說有個原則然後照做就好。

接下來,我們透過範例,來實際體會如何應用 SRP 到 React 元件中。

https://codesandbox.io/s/resume-legacy-9htc5?file=/src/Resume.js

原始版本:一個履歷元件

範例程式碼: https://codesandbox.io/s/resume-legacy-9htc5?file=/src/Resume.js

在 Codesandbox 可以看到原始碼以及相對應的介面,此範例已經有初步把一些簡單的元件拆出來比如 TitleSection, Attachment, BasicInfo 等等。但基本上 Resume.js 就是一個什麼都做的元件,需要自己去拉資料、也要負責處理載入狀態的呈現、還有資料的排版等。如果需要更好維護的話我們還要再進一步的拆解,以方便未來修改時,僅需要修改相對應的部分即可。

第二版: 資料處理與資料呈現

為了拆得更乾淨,我們要先從另一個角度來看此元件的流程,如下圖:

流程圖提供了我們切分元件的依據,依據上圖我們可以把顯示履歷資訊切出來變成一個資訊呈現的元件,其他的仍舊保留在原本的元件內,如此變成:

  1. 狀態控制、資料流與元件 Layout
  2. 資訊呈現的元件

是的其實就是 container / presentational component,或是更傳統的 MVC 中的 V&C,把資料呈現與資料處理切開來做,我們把履歷內容相關的全部搬移到 ResumePresenter.js,變成第二個版本 https://codesandbox.io/s/resume-container-presenter-xj7wm

https://codesandbox.io/s/resume-container-presenter-xj7wm

再來複習一次 SRP 的核心概念:A class should have one, and only one, reason to change. 一個元件只有唯一一個變更的理由,這句話其實從另一角度來解釋就是『一個變更的需求只需要變更到一個元件』。第二版依據資料流找出不同的責任範圍,把狀態控制、資料流 (ResumeBuilder) 與資訊呈現 (ResumeContent) 拆開,未來如果需要調整履歷呈現的格式比如各區塊的順序/結構調整,就只要改 ResumeContent ,也不用怕動到資料流(前提:拆得夠乾淨、且有用測試保護),同樣的如果我想調整資料流,比如再加另外一個 API call,那就只要修改 ResumeBuilder 即可。

第三版: 跨元件共用邏輯

如果想再乾淨一點呢?回到元件的資料流來看,載入狀態與載入畫面是否也能有切出來的可能?由於載入畫面是滿泛用的一個功能,我們在切出元件時,是否有辦法在包成一個通用邏輯呢?在這個範例內我想做成一個 HOC,此 HOC 功能如下圖:讓元件可以自動依據載入狀態切換要顯示載入畫面或者是實際的內容元件。對 HOC 概念不熟者可以參考 React 官方文件

https://codesandbox.io/s/resume-loadinghoc-container-presenter-5x4k6?file=/src/LoadableHOC.js

我把這個 HOC 命名為 LoadableHOC,未來只要元件有載入狀態的介面的需求,就可以直接包覆此 HOC,即可擁有統一的載入呈現方式,附加的好處是如果有需要改載入流程或介面時,也可以非常輕鬆的修改。如果不需要此功能,也就是把 HOC 拔掉而已。

https://codesandbox.io/s/resume-loadinghoc-container-presenter-5x4k6

至此我們有了三個元件:

  • Resume 負責資料的處理
  • ResumePresenter 負責履歷資訊的呈現
  • LoadableHOC 負責載入狀態的管理與呈現

相較於原始版本,被拆出了三個各自獨立且明確的責任,未來只要履歷資料流相關的就到 Resume.js、履歷呈現相關的就修改 ResumePresenter.js 而載入狀態管理就是 LoadableHOC.js。未來相對應的功能的維護只需要到相對應的檔案內去調整,因而是更好維護的一種結構。

遺留程式碼 Legacy Code

一般專案 Legacy Code 可能動輒數百行數千行,或是切了幾個元件但是期間的耦合度很高,此時要一步達到 SRP 是不太可能,面對這種狀況有兩種策略:

大卸N塊

如果 Legacy Code 本身早有明確的幾個功能區分,且這些功能元件間沒有太高的耦合度,則可以依照這些功能大致的切出幾個子元件,再來往下切分。

凌遲

如果功能區分不明確,也就是功能間的耦合度高,就只能凌遲處理,也就是就自己正在修改的部分,逐步建立測試,劃分出合適的功能區塊,並且從原有的程式碼中拆解出來。

比較髒的專案通常是後者,但無論是大卸N塊或是凌遲都需要良好的重構技巧,因此對於重構的掌握度也是個很重要的練習。

真實專案比這複雜許多

由於功能的改變,元件可能長大、改變、甚或棄用。每次的功能改變,都可以好好的思考是否拆出新的元件或是移除不合宜的元件。拆分元件時需要注意:獨立與明確的責任,切忌因為程式碼很冗長就無腦拆,如果拆出的元件彼此之間藕荷度還是很高,其實對可維護性沒有太大的幫助,只是創造出越來越多散亂的小零件。至於如何切出獨立與明確的責任,畫出元件的流程圖是個好方法,透過流程圖辨別出不同階段在做的不同事情,等於是劃出不同的責任,接著就可以依照該責任拆出獨立的元件。

本文的範例已經經過大幅度的簡化,重點是希望透過三個版本的變化讓大家去體會 SRP 原則的應用與結果。當然實際專案不會這麼的單純,還會有許多相依性、共用等等的問題,這就需要接下來的其他原則來幫忙。

延伸思考

  • 依據單一責任原則,你認為這裡所謂的責任應該要切多大?用什麼樣的依據去切分?
  • 本次範例使用的是 class component,如果改以 functional component + hooks + suspense 會有什麼不一樣的做法嗎?

參考資料

--

--