[不是工程師] 物件導向設計原則(SOLID)很繞口?試試從模組化開發與測試來理解吧!
程式總是寫成一大坨導致開發進度緩慢?是時候開始用模組化概念來開發與測試了!
作者: Vincent Ke
更新日期:
「這個功能好像跟上次做的一樣欸?」
「那這次開發起來應該很快吧?」
「不是程式碼貼一貼就好了嗎?」
透過前人的經驗,將類似的功能開發在不同的產品或專案上, 似乎已經成為多數公司的盈利模式之一,的確, 有前人下過的苦工和踩過的地雷之後,在下一次的開發上, 的確可以較快上手,但保證是這樣嗎?
舉例來說,一樣是使用到地圖介面, 但訂房網站與行程規劃網站之中的商業邏輯與Know how就大相徑庭,儘管都是使用到圖台介面, 也會隨著查詢資料或是Meta Data上的不同,造成開發上的思維也會不全一樣。
當然,就算是一樣的功能需求,在同一間公司裡, 只要是執行開發的人不同, 就不一定真的可以節省到實際的開發時間了, 更別提不同的商業邏輯還會有不同的測試Scope呢。但理論上,只要相似或雷同的的功能, 也應該要以有效縮短開發期程為目標,那究竟要施展什麼魔法, 讓理論與現實不會差距過遠呢
先把開發丟一旁,先理解模組化(module / assumbly / package )開發的概念吧!
模組化其實就是把大而冗長的程式碼, 試著拆解成不同區塊的小程式來個別運作, 讓大功能變成個別的小模組, 而小模組可以再細分的話就拆解成更小的模組, 並確保模組們可以各自獨立運作。
例如我們如果每造一隻手錶就是一個Project, 難道一定要從時針、分針、甚至到機心都得重新設計嗎? 如果我們可以把零件各自獨立出來,讓機心、錶殼、 甚至是錶帶可以個別開發,只要最後可以確保組裝完成, 並且各自的運作從獨立到整體都可以運作順暢, 那就是一個好的模組化。
在公司接下到一個手錶專案後,也許只要依據專案與規格、 替換不同的錶殼與錶帶,但機心的需求是相同的, 這樣至少機心部分的設計與開發,就可以省下不少力氣與時間了
所以模組化的特性就是所謂的「獨立性」與「重複使用性」, 讓每個模組各自獨立,並且可以依據需求個別擴充, 但保證彼此之間可以互相兼容,來讓未來的其他專案可以重複使用, 就是模組化最大的價值所在。
但好處可不只有節省開發單位的力氣, 模組化開發也可以有效節省測試時間, 因為每個模組的自動化測試都各自獨立,並沒有專案上的差別, 唯一只有差在整合性測試上耗費的時間呢!
但當然這些都只是概念,而要讓概念可以達到最大的實踐效益, 就必須在整個架構設計上達到幾個概念:
- 可拆解性及可合成性:功能必須個別拆開,
才有辦法達到模組化的要求;當然最後獨立運作的個體, 也必須要可以由下而上的結合,才有辦法滿足開發的原始需求。
- 獨立性:讓每個模組發生的問題都可以個別處理,單獨修改、
擴充與維護,並不和第一點做出衝突,才是好的模組化設計, 例如錶帶和機心的問題可以個別處理,並不會交互影響。
- 介面的明確性:介面與介面之間,必須易於理解,
相互依賴的關係也必須明確,才會在模組的結合上方便許多, 就像輪匡和輪胎有明確的規格可以結合一樣。
- 資訊的隱匿性:只公開必要的資訊,
把資料處理的邏輯都塞在模組內解決,讓組合的方式單一、單純, 才會是一個好的模組;反之,如果太多的規格或資訊外露, 就會造成模組間溝通的障礙與困難。 就像如果桌機的顯卡如果有太多種不同的規格, 又各自支援不同的系統,那我們在購買或組裝時,只會更手忙腳亂吧!
除了上面的口語化說法之外,在程式設計中也有由Robert C. Martin所提出、非常有名的導入物件導向設計的五個原則(英文縮寫為SOLID):
S:單一功能原則(Single responsibility):每一個物件應該僅具有一種單一功能的概念, 舉例方向盤模組就是控制方向,引擎模組就是負責發動及前進.. 等等。這樣的好處就是可以提昇物件的易維護性, 避免車壞了卻找不到到底是哪裡壞掉的問題。
校正小編補充:
先不論是否有正確實現,但是有沒有單一功能原則的一大特徵就是Class(類別)很多,專案檔裡面有很多檔案。特別提出來是因為常常新手好像很怕新增檔案一樣,一個檔案的程式都塞滿滿。如果實現單一功能原則有可能很多Class裡面沒有幾行。並且在單一功能原則下的物件一般會很容易進行測試。
先不論是否有正確實現,但是有沒有單一功能原則的一大特徵就是Class(類別)很多,專案檔裡面有很多檔案。特別提出來是因為常常新手好像很怕新增檔案一樣,一個檔案的程式都塞滿滿。如果實現單一功能原則有可能很多Class裡面沒有幾行。並且在單一功能原則下的物件一般會很容易進行測試。
O:開閉原則(Open-Closed):物件導向原則中,堅持「 軟體對於外部擴充是開放的,但是對於內部的修改封閉的」的概念, 而這樣的概念也是避免內部的修改造成物件的複雜化。 所以當你的物件有「新的需求」時, 並非是改動原本的物件或是原本的程式碼, 而是必須透過擴充的方式來完成需求。
校正小編補充:
這其實還滿難的,因為依照這原則程式碼可能會「發散」。比方說常見的程式端的資料庫處理,也可以想成就是MVC的Model層。如果以PHP習慣的方式切成 Model / Repository / Service,Model只是個mapping的entity,Repository才是SQL command的集合,Service則是某項服務,可以想成Repository 加上 商業邏輯。
所以出現一個新需求時,通常代表的就是Service會多一個method。Repository也可能會多幾個method,而在這個開閉原則下,你「不會」去改變原本在Repository裡面的method讓它可以一起處裡新需求,因為可能會出現Side effect(副作用或稱改壞了)。這樣的改動固然安全,但是時間一久了你會發現很多同性質但是只差一點點的method,程式碼多出很多。
當然這時你可能會想重構它們,把它們「束」起來用parameter來去分工用。不過目前的開發趨勢與經驗上會告訴你盡量不要,通常真的把它們合併只會讓測試更難測而已(尤其是unit test)。後期開發修改也更容易產生Side effect。
這其實還滿難的,因為依照這原則程式碼可能會「發散」。比方說常見的程式端的資料庫處理,也可以想成就是MVC的Model層。如果以PHP習慣的方式切成 Model / Repository / Service,Model只是個mapping的entity,Repository才是SQL command的集合,Service則是某項服務,可以想成Repository 加上 商業邏輯。
所以出現一個新需求時,通常代表的就是Service會多一個method。Repository也可能會多幾個method,而在這個開閉原則下,你「不會」去改變原本在Repository裡面的method讓它可以一起處裡新需求,因為可能會出現Side effect(副作用或稱改壞了)。這樣的改動固然安全,但是時間一久了你會發現很多同性質但是只差一點點的method,程式碼多出很多。
當然這時你可能會想重構它們,把它們「束」起來用parameter來去分工用。不過目前的開發趨勢與經驗上會告訴你盡量不要,通常真的把它們合併只會讓測試更難測而已(尤其是unit test)。後期開發修改也更容易產生Side effect。
L:里氏替換原則(Liskov substitution):程式中的多個物件,
校正小編補充:
這其實一般不用去太考慮他,畢竟現在的程式語言幾乎都有實作他。不過如果用JavaScript的話可能就需要比較注意,因為JavaScript不是一個完整的物件導向是語言。所以有可能發生子類別再不正確的繼承下無法替代父類別。但JavaScript即使是ES6的Class也是用原型鏈模擬的,所以其實一開始就要有這層認知。
這其實一般不用去太考慮他,畢竟現在的程式語言幾乎都有實作他。不過如果用JavaScript的話可能就需要比較注意,因為JavaScript不是一個完整的物件導向是語言。所以有可能發生子類別再不正確的繼承下無法替代父類別。但JavaScript即使是ES6的Class也是用原型鏈模擬的,所以其實一開始就要有這層認知。
I:介面隔離原則(Interface segregation):因為用戶的需求不同, 我們也必須開放其對應需求的介面,來提拱使用。 好處是將需求作出有效的介面區分,來避免因為不相關的需求, 造成必須一同更動介面設計的問題。
校正小編補充:
如果說開閉原則是個大方向的話,介面隔離就比較像是實際的指示。就像HDMI線與網路線的功用不太一樣。但因為外型上的孔洞就不一樣,所以實際上幾乎不可能插錯。如果程式間的連結也是使用interface(介面)來實作的話,也可以有效地避免錯誤使用的情況。
如果說開閉原則是個大方向的話,介面隔離就比較像是實際的指示。就像HDMI線與網路線的功用不太一樣。但因為外型上的孔洞就不一樣,所以實際上幾乎不可能插錯。如果程式間的連結也是使用interface(介面)來實作的話,也可以有效地避免錯誤使用的情況。
D:依賴反轉原則(Dependency inversion):例如在A模組的內部中使用到了B模組, 那A為高階模組,B為低階模組(因為他是A的組成之一), A必須透過B才能完成某種功能,這是一種依賴關係, 但A和B不應該是被綁死才能作為實現功能的一種方法, 所以依賴的原則要用抽象的角度來解析,也就是實現功能的目的, 而並非功能實作的物件本體。就像人吃飯就可以填飽肚子, 但填飽肚子是目的,而並非一定要靠吃飯才可以, 吃麵不也是一種選擇嗎?所以人和飯沒有依賴關係, 有依賴關係的應該是「可以填飽肚子的食物」, 才能實現我們的目的。
校正小編補充:
這最常見的應該就是資料庫的使用吧!如果依賴反轉做得好的話,你不會在意資料是從MySQL還是Postgre SQL來的,或是其實是從NoSQL或是Cache來的。甚至是從Open Data的Web API 或是 Firebase來的。總之你做的就是做了一個介面,並且丟進去一個可以透過該介面撈到資料的服務。
這最常見的應該就是資料庫的使用吧!如果依賴反轉做得好的話,你不會在意資料是從MySQL還是Postgre SQL來的,或是其實是從NoSQL或是Cache來的。甚至是從Open Data的Web API 或是 Firebase來的。總之你做的就是做了一個介面,並且丟進去一個可以透過該介面撈到資料的服務。
有發現其實常常想法都很類似嗎?畢竟當大師歸納出固定原則之前也都是學習了各種流派的想法。也有很多時候是從其他產業借鏡的。因此很多時候,當看到像「SOLID」這樣的專有名詞其實不用怕,很多的概念早就已經融入生活中各式各樣的行為當中(例如,手機充電器的統一規格)。
最後,如果你喜歡我們的文章,別忘了到我們的FB粉絲團按讚喔!!