使用TDD實踐SOLID
我們知道好的OOP並不是急著馬上套Design Pattern,而是先遵守SOLID原則;也知道TDD教我們先寫測試,將來可以安心的重構。但我們實務上該如何實踐SOLID呢?事實上,TDD不僅僅是先寫測試
而已,伴隨著TDD的開發流程,會讓我們會不知不覺的實踐SOLID。
SOLID
SOLID是OOP以下幾個原則的縮寫 :
- S : Single Responsibility Principle (單一職責原則)
- O : Open Closed Principle (開放封閉原則)
- L : Liskov Substitution Principle (里氏替換原則),Least Knowledge Principle (最小知識原則)
- I : Interface Segregation Principle (介面隔離原則)
- D : Dependency Inversion Principle (依賴反轉原則)
Laravel的作者Taylor Otwell曾有一段話 : 1 1詳細請參考Laravel之父 : 學習出色的Design Pattern
如果有人想成為更棒的PHP工程師,你會怎麼建議?
學習出色的Design Pattern。這不只適用在PHP。你可以在任何程式語言使用這些pattern。尤其是SOLID。把這五個徹底學好,它會把你帶到新的境界,我每次寫code幾乎都在想這五個。
除了Design Pattern,重點在於更根本的SOLID,這5點才是OOP的心法。
TDD
TDD Testing Driven Development (測試驅動開發) 顛覆大家以往的習慣,強調先寫測試,再寫程式
,整個流程是 :
寫測試
是為了重構
,這點沒有問題,但很多人的疑問是該先寫測試
還是後寫測試
?
實務上該如何SOLID?
這正是大家的痛點,OOP語法都會,SOLID原則也懂,但真的上場就是寫不出符合SOLID原則的OOP。
事實上我們需要的是TDD的開發流程
,讓我們不知不覺就實踐SOLID了。
TDD與SOLID
我們來看看幾個實務上常遇到的問題 :
SRP 單一職責原則
為什麼測試會加不上去呢?通常原因如下 :
引用一下91哥上課的名言 : 2 2詳細請參考自動測試與TDD開發實務(使用C#)
如果你的程式,測試加不上去,測試很難寫,很難獨立對它進行測試,那代表你production code寫太爛。
白話就是你的程式違反了SRP 單一職責原則,功能太多太複雜,所以測試寫不下去。
但若你走的是TDD開發流程,因為先寫測試
,所以你會以測試好寫
為前提去寫程式,為了測試好寫,你會希望功能單純
,你會希望相依物件少
,這樣測試才好寫啊,對!為了測試好寫
,你會不知不覺走向SRP 單一職責原則,這也是為什麼相同功能的code,先寫測試
與後寫測試
會寫出來的code風格完全不一樣。
DIP 依賴反轉原則
為什麼無法隔離呢?通常原因如下 :
白話就是你的程式違反了DIP 依賴反轉原則,高層物件直接相依了低層物件,應該反過來,相依的物件應該由高層來決定,也就是使用Dependency Injection 依賴注入。
但若你走的是TDD開發流程,為了隔離測試
,為了能將mock的假物件注入,你會不知不覺走向DIP 依賴反轉原則,使用依賴注入
的方式,這也是為什麼相同功能的code,先寫測試
與後寫測試
處理相依物件方式完全不一樣。
LKP 最小知識原則
為什麼自己都不知道呢?通常原因如下 :
白話就是你的程式違反了LKP 最小知識原則,你要求使用者必須相依很多物件,知道很多細節才能用你的API,因此API很難用。
但若你走的是TDD開發流程,因為先寫測試
,所以你會以測試好寫
的角度去設計API,為了測試好寫,你會希望相依物件少
,你會希望不要知道細節
,這樣測試才好寫啊,對!為了測試好寫
,你會不知不覺走向LKP 最小知識原則,這也是為什麼相同功能的code,先寫測試
與後寫測試
寫出來的API風格完全不一樣。
OCP 開放封閉原則
為什麼不斷加上if...else
,造成測試寫不下去呢?通常原因如下 :
白話就是你的程式違反了OCP 開放封閉原則,對於需求經常增加的部分,應該提出interface,只考慮抽象層級的介面互動,把新增功能委託給其他class去處理,如此對於擴展是開放
的,但對於修改是封閉
的,因此不會修改到原來的邏輯。最典型的就是應用程式的外掛,你可以利用外掛增加功能,但主程式都不用更新修改。
但若你走的是TDD開發流程,因為先寫測試
,所以你會以測試好寫
為前提去寫程式,若程式有一堆if..else
,尤其是巢狀if..else
,每多一層,你的測試案例就會以2的n次方個數成長,呈現測試案例爆炸,對!為了測試好寫
,你會不知不覺走向OCP 開放封閉原則,這也是為什麼相同功能的code,先寫測試
與後寫測試
寫出來的code的可測試性完全不一樣。
ISP 介面隔離原則
為什麼class會有interface的空實作呢?通常原因如下 :
白話就是你的程式違反了ISP 介面隔離原則,你訂出了超過需求的interface,所以才會出現空實作
,通常出現在從既有class抽出interface,這種interface是以實作的角度去建立,而非需求的角度所建立。
但若你走的是TDD開發流程,因為先寫測試
,所以會以需求
與測試案例
的角度去寫測試,為了符合需求,你訂出的interface會以需求的角度去思考,而非以實作的角度去思考,你會不知不覺走向ISP 介面隔離原則,這也是為什麼相同功能的code,先寫測試
與後寫測試
所訂出的interface會完全不一樣。
LSP 里氏替換原則
為什麼一切換class就爆掉了?通常原因如下 :
白話就是你的程式違反了LSP 里氏替換原則,在切換class之前,原本預期你會實作interface所定義的功能,但切換class之後,才發現它骨子卻去做其他的事情,因此只要一切換class就爆掉了。
TDD的先寫測試
並沒有辦法幫助你實現LSP 里氏替換原則,但因為你有寫測試,只要違反LSP 里氏替換原則,一跑測試一定會亮紅燈
,所以可以幫助你及早發現問題加以修改,而不用等QA測試時才發bug report。
Conclusion
- TDD開發流程讓我們不知不覺的實現SOLID,讓程式體質變好,而且透過TDD的重構,還會使得設計模式自然出現,而非一開始就使用設計模式而over design。
- TDD讓我們實現SOLID成為可能,只要依循TDD的開發流程,就可以不知不覺的實現SOLID。