使用TDD實踐SOLID

我們知道好的OOP並不是急著馬上套Design Pattern,而是先遵守SOLID原則;也知道TDD教我們先寫測試,將來可以安心的重構。但我們實務上該如何實踐SOLID呢?事實上,TDD不僅僅是先寫測試而已,伴隨著TDD的開發流程,會讓我們會不知不覺的實踐SOLID。

SOLID


SOLID是OOP以下幾個原則的縮寫 :

  1. S : Single Responsibility Principle (單一職責原則)
  2. O : Open Closed Principle (開放封閉原則)
  3. L : Liskov Substitution Principle (里氏替換原則),Least Knowledge Principle (最小知識原則)
  4. I : Interface Segregation Principle (介面隔離原則)
  5. D : Dependency Inversion Principle (依賴反轉原則)

Laravel的作者Taylor Otwell曾有一段話 : 1 1詳細請參考Laravel之父 : 學習出色的Design Pattern

如果有人想成為更棒的PHP工程師,你會怎麼建議?

學習出色的Design Pattern。這不只適用在PHP。你可以在任何程式語言使用這些pattern。尤其是SOLID。把這五個徹底學好,它會把你帶到新的境界,我每次寫code幾乎都在想這五個。

Taylor Otwell

除了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 2詳細請參考自動測試與TDD開發實務(使用C#)

如果你的程式,測試加不上去,測試很難寫,很難獨立對它進行測試,那代表你production code寫太爛。

91哥

白話就是你的程式違反了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。
2015-11-28