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