進度條

[不是工程師] 物件導向設計原則(SOLID)很繞口?試試從模組化開發與測試來理解吧!

程式總是寫成一大坨導致開發進度緩慢?是時候開始用模組化概念來開發與測試了!

作者: Vincent Ke 更新日期:
「這個功能好像跟上次做的一樣欸?」
「那這次開發起來應該很快吧?」
「不是程式碼貼一貼就好了嗎?」
 
 
 
透過前人的經驗,將類似的功能開發在不同的產品或專案上,似乎已經成為多數公司的盈利模式之一,的確,有前人下過的苦工和踩過的地雷之後,在下一次的開發上,的確可以較快上手,但保證是這樣嗎?
 
 
舉例來說,一樣是使用到地圖介面,但訂房網站與行程規劃網站之中的商業邏輯與Know how就大相徑庭,儘管都是使用到圖台介面,也會隨著查詢資料或是Meta Data上的不同,造成開發上的思維也會不全一樣。
 
 
當然,就算是一樣的功能需求,在同一間公司裡,只要是執行開發的人不同,就不一定真的可以節省到實際的開發時間了,更別提不同的商業邏輯還會有不同的測試Scope呢。但理論上,只要相似或雷同的的功能,也應該要以有效縮短開發期程為目標,那究竟要施展什麼魔法,讓理論與現實不會差距過遠呢
 
 
 
先把開發丟一旁,先理解模組化(module / assumbly / package )開發的概念吧!
 
 
 
 
 
模組化其實就是把大而冗長的程式碼,試著拆解成不同區塊的小程式來個別運作,讓大功能變成個別的小模組,而小模組可以再細分的話就拆解成更小的模組,並確保模組們可以各自獨立運作。
 
 
例如我們如果每造一隻手錶就是一個Project,難道一定要從時針、分針、甚至到機心都得重新設計嗎?如果我們可以把零件各自獨立出來,讓機心、錶殼、甚至是錶帶可以個別開發,只要最後可以確保組裝完成,並且各自的運作從獨立到整體都可以運作順暢,那就是一個好的模組化。
 
 
在公司接下到一個手錶專案後,也許只要依據專案與規格、替換不同的錶殼與錶帶,但機心的需求是相同的,這樣至少機心部分的設計與開發,就可以省下不少力氣與時間了
 
 
 
 
 
所以模組化的特性就是所謂的「獨立性」與「重複使用性」,讓每個模組各自獨立,並且可以依據需求個別擴充,但保證彼此之間可以互相兼容,來讓未來的其他專案可以重複使用,就是模組化最大的價值所在。
 
但好處可不只有節省開發單位的力氣,模組化開發也可以有效節省測試時間,因為每個模組的自動化測試都各自獨立,並沒有專案上的差別,唯一只有差在整合性測試上耗費的時間呢!
 
 
 
 
 
但當然這些都只是概念,而要讓概念可以達到最大的實踐效益,就必須在整個架構設計上達到幾個概念:
 
  1. 可拆解性及可合成性:功能必須個別拆開,才有辦法達到模組化的要求;當然最後獨立運作的個體,也必須要可以由下而上的結合,才有辦法滿足開發的原始需求。
     
  2. 獨立性:讓每個模組發生的問題都可以個別處理,單獨修改、擴充與維護,並不和第一點做出衝突,才是好的模組化設計,例如錶帶和機心的問題可以個別處理,並不會交互影響。
     
  3. 介面的明確性:介面與介面之間,必須易於理解,相互依賴的關係也必須明確,才會在模組的結合上方便許多,就像輪匡和輪胎有明確的規格可以結合一樣。
     
  4. 資訊的隱匿性:只公開必要的資訊,把資料處理的邏輯都塞在模組內解決,讓組合的方式單一、單純,才會是一個好的模組;反之,如果太多的規格或資訊外露,就會造成模組間溝通的障礙與困難。就像如果桌機的顯卡如果有太多種不同的規格,又各自支援不同的系統,那我們在購買或組裝時,只會更手忙腳亂吧!
 
 
 
 
 
除了上面的口語化說法之外,在程式設計中也有由Robert C. Martin所提出、非常有名的導入物件導向設計的五個原則(英文縮寫為SOLID):
 
 
S:單一功能原則(Single responsibility):每一個物件應該僅具有一種單一功能的概念,舉例方向盤模組就是控制方向,引擎模組就是負責發動及前進..等等。這樣的好處就是可以提昇物件的易維護性,避免車壞了卻找不到到底是哪裡壞掉的問題。

 
校正小編補充:

先不論是否有正確實現,但是有沒有單一功能原則的一大特徵就是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。
 
 
 

L:里氏替換原則(Liskov substitution):程式中的多個物件,是可以在不改變程式正確性的前提下,被其物件的子類別所替換的概念。也就是父類別出現的地方,我們就必須期待子類別就能代替它,而且要能替換而不發生錯誤與異常。

 
校正小編補充:

這其實一般不用去太考慮他,畢竟現在的程式語言幾乎都有實作他。不過如果用JavaScript的話可能就需要比較注意,因為JavaScript不是一個完整的物件導向是語言。所以有可能發生子類別再不正確的繼承下無法替代父類別。但JavaScript即使是ES6的Class也是用原型鏈模擬的,所以其實一開始就要有這層認知。
 
 

 

I:介面隔離原則(Interface segregation):因為用戶的需求不同,我們也必須開放其對應需求的介面,來提拱使用。好處是將需求作出有效的介面區分,來避免因為不相關的需求,造成必須一同更動介面設計的問題。
 
校正小編補充:

如果說開閉原則是個大方向的話,介面隔離就比較像是實際的指示。就像HDMI線與網路線的功用不太一樣。但因為外型上的孔洞就不一樣,所以實際上幾乎不可能插錯。如果程式間的連結也是使用interface(介面)來實作的話,也可以有效地避免錯誤使用的情況。

 
 
D:依賴反轉原則(Dependency inversion):例如在A模組的內部中使用到了B模組,那A為高階模組,B為低階模組(因為他是A的組成之一),A必須透過B才能完成某種功能,這是一種依賴關係,但A和B不應該是被綁死才能作為實現功能的一種方法,所以依賴的原則要用抽象的角度來解析,也就是實現功能的目的,而並非功能實作的物件本體。就像人吃飯就可以填飽肚子,但填飽肚子是目的,而並非一定要靠吃飯才可以,吃麵不也是一種選擇嗎?所以人和飯沒有依賴關係,有依賴關係的應該是「可以填飽肚子的食物」,才能實現我們的目的。
 
校正小編補充:

這最常見的應該就是資料庫的使用吧!如果依賴反轉做得好的話,你不會在意資料是從MySQL還是Postgre SQL來的,或是其實是從NoSQL或是Cache來的。甚至是從Open Data的Web API 或是 Firebase來的。總之你做的就是做了一個介面,並且丟進去一個可以透過該介面撈到資料的服務。
 
 


有發現其實常常想法都很類似嗎?畢竟當大師歸納出固定原則之前也都是學習了各種流派的想法。也有很多時候是從其他產業借鏡的。因此很多時候,當看到像「SOLID」這樣的專有名詞其實不用怕,很多的概念早就已經融入生活中各式各樣的行為當中(例如,手機充電器的統一規格)。

 
 

最後,如果你喜歡我們的文章,別忘了到我們的FB粉絲團按讚喔!!

圖文系列教學: 不是工程師也可以看得懂的程式名詞解說!

Medium vincent

Vincent Ke

喜歡把混亂的事情變的簡單 用嘴巴做事其實很可以 但要結合靈活的腦袋思考 就一起來拆解吧