讓我們從最後一個 SOLID 原則開始吧,即依賴倒置原則(Dependency Inversion Principle,簡稱 DIP)(不要和依賴注入Dependency Injection ,DI 弄混淆了)。這個原則所說的是高階模組不應該依賴具象的低階模組,它們都應該依賴相應模組的抽象層。
我仍將使用自行車的示例來嘗試給你解釋這個原則。首選看下這個 Bike
介面:
interface Bike {
void pedal()
void backPedal()
}
複製程式碼
MountainBike
和 ClassicBike
這兩個類實現了上面的介面:
// 山地車
class MountainBike implements Bike {
override void pedal() {
// complex code that computes the inner workings of what happens
// when pedalling on a mountain bike, which includes taking into
// account the gear in which the bike currently is.
}
override void backPedal() {
// complex code that computes what happens when we back pedal
// on a mountain bike, which is that you pedal in the wrong
// direction with no discernible effect on the bike
}
}
// 傳統自行車
class ClassicBike implements Bike {
override void pedal() {
// the same as for the mountain bike with the distinction that
// there is a single gear on a classic bike
}
override void backPedal() {
// complex code that actually triggers the brake function on the
// bike
}
}
複製程式碼
正如你所看到的,踩腳踏板(pedal)會讓自行車向前行駛,但是山地車 MountainBike
因為有多個齒輪,所以它的 pedal 會更加複雜。另外,向後踩腳踏板(back pedal)時,山地車不會做任何事,而傳統自行車 ClassicBike
則會觸發剎車操作。
我之所以在每個方法的註釋中都有提到“complex code”,是因為我想指出我們應該把上述程式碼移動到不同的模組中。我們這樣做是為了簡化自行車類以及遵循單一職責原則(自行車類不應該擔起在你向前或向後踩腳踏板時究竟發生了什麼的計算工作,它們應該處理有關自行車的更高階別的事情)。
為了做到這一點,我們將為每種型別的 pedalling 建立一些行為類。
class MountainBikePedalBehaviour {
void pedal() {
//complex code
}
}
class MountainBikeBackPedalBehaviour {
void backPedal() {
// complex code
}
}
class ClassicBikePedalBehaviour {
void pedal() {
// complex code
}
}
class ClassicBikeBackPedalBehaviour {
void backPedal() {
// complex code
}
}
複製程式碼
然後像下面這樣使用這些類:
// 山地車
class MountainBike implements Bike {
override void pedal() {
var pedalBehaviour = new MountainBikePedalBehaviour()
pedalBehaviour.pedal()
}
override void backPedal() {
var backPedalBehaviour = new MountainBikeBackPedalBehaviour()
backPedalBehaviour.backPedal()
}
}
// 傳統自行車
class ClassicBike implements Bike {
override void pedal() {
var pedalBehaviour = new ClassicBikePedalBehaviour()
pedalBehaviour.pedal()
}
override void backPedal() {
var backPedalBehaviour = new ClassicBikeBackPedalBehaviour()
backPedalBehaviour.backPedal()
}
}
複製程式碼
這個時候,我們可以很清楚地看到高階模組 MountainBike
依賴於某些具體的低階模組 MountainBikePedalBehaviour
和 MountainBikeBackPedalBehaviour
。ClassicBike
以及它的低階模組同樣如此。根據依賴倒置原則,高階模組和低階模組都應該依賴抽象。為此,我們需要以下介面:
interface PedalBehaviour {
void pedal()
}
interface BackPedalBehaviour {
void backPedal()
}
複製程式碼
除了需要實現上面的介面外,行為類的程式碼與之前無異:
class MountainBikePedalBehaviour implements PedalBehaviour {
override void pedal() {
// same as before
}
}
複製程式碼
剩下的其他行為類同上。
現在我們需要一種方法將 PedalBehaviour
和 BackPedalBehaviour
傳遞給 MountainBike
和 ClassicBike
類。我們可以選擇在構造方法、pedal()
、pedalBack()
中完成這件事。本例中,我們使用構造方法。
class MountainBike implements Bike {
PedalBehaviour pedalBehaviour;
BackPedalBehaviour backPedalBehaviour;
public MountainBike(PedalBehaviour pedalBehaviour,
BackPedalBehaviour backPedalBehaviour) {
this.pedalBehaviour = pedalBehaviour;
this.backPedalBehaviour = backPedalBehaviour;
}
override void pedal() {
pedalBehaviour.pedal();
}
override void backPedal() {
backPedalBehaviour.backPedal();
}
}
複製程式碼
ClassicBike
類同上。
我們的高階模組(MountainBike
和 ClassicBike
)不再依賴於具體的低階模組,而是依賴於抽象的 PedalBehaviour
和 BackPedalBehaviour
。
在我們的例子中,我們應用的主模組可能看起來向下面這樣:
class MainModule {
MountainBike mountainBike;
ClassicBike classicBike;
MountainBikePedalBehaviour mountainBikePedalBehaviour;
ClassicBikePedalBehaviour classicBikePedalBehaviour;
MountainBikeBackPedalBehaviour mountainBikeBackPedalBehaviour;
ClassicBikeBackPedalBehaviour classicBikeBackPedalBehaviour;
public MainModule() {
mountainBikePedalBehaviour = new MountainBikePedalBehaviour();
mountainBikeBackPedalBehaviour =
new MountainBikeBackPedalBehaviour();
mountainBike = new MountainBike(mountainBikePedalBehaviour,
mountainBikeBackPedalBehaviour);
classicBikePedalBehaviour = new ClassicBikePedalBehaviour();
classicBikeBackPedalBehaviour =
new ClassicBikeBackPedalBehaviour();
classicBike = new ClassicBike(classicBikePedalBehaviour,
classicBikeBackPedalBehaviour);
}
public void pedalBikes() {
mountainBike.pedal()
classicBike.pedal()
}
public void backPedalBikes() {
mountainBike.backPedal();
classicBike.backPedal();
}
}
複製程式碼
可以看到,我們的 MainModule
依賴了具體的低階模組而不是抽象層。我們可以通過向構造方法中傳遞依賴來改善這種情況:
public MainModule(Bike mountainBike, Bike classicBike, PedalBehaviour mBikePB, BackPedalBehaviour mBikeBPB, PedalBehaviour cBikePB, BackPedalBehaviour cBikeBPB)...
複製程式碼
現在,MainModule
部分依賴了抽象層,部分依賴了低階模組,這些低階模組也依賴了那些抽象層。所有這些模組之間的關係不再依賴於實現細節。
在我們到達應用程式中的最高模組之前,為了儘可能地延遲一個具體類的例項化,我們通常要靠依賴注入和實現了依賴注入的框架。你可以在 這裡 找到更多有關依賴注入的資訊。我們可以將依賴注入視為幫助我們實現依賴倒置的工具。我們不斷地向依賴鏈中傳遞依賴關係以避免具體類的例項化。
那麼為什麼要經歷這一切呢?不依賴於具象的一個優點就是我們可以模擬一個類,從而使測試更容易進行。我們來看一個簡單的例子。
interface Network {
public String getServerResponse(URL serverURL);
}
class NetworkRequestHandler implements Network {
override public String getServerResponse(URL serverURL) {
// network code implementation
}
}
複製程式碼
假設我們還有一個 NetworkManager
類,它有一個公共方法,通過使用一個 Network
的例項返回伺服器響應:
public String getResponse(Network networkRequestHandler, URL url) {
return networkRequestHandler.getServerResponse(url)
}
複製程式碼
因為這樣的程式碼結構,我們可以測試程式碼如何處理來自伺服器的“404”響應。為此,我們將創 NetworkRequestHandler
的模擬版本。我們之所以可以這麼做,是因為 NetworkManager
依賴於抽象層,即 Network
,而不是某個具體的 NetworkRequestHandler
。
class Mock404 implements Network {
override public String getServerResponse(URL serverURL) {
return "404"
}
}
複製程式碼
通過呼叫 getResponse
方法,傳遞 Mock404
類的例項,我們可以很容易地測試我們期望的行為。像 Mockito 這樣的模擬庫可以幫助你模擬某些類,而無需編寫單獨的類來執行此操作。
除了易於測試,我們的應用在多變情景下也能應對自如。因為模組之間的關係是基於抽象的,我們可以更改具體模組的實現,而無需大範圍地更改程式碼。
最後同樣重要的是這會讓事情變得更簡單。如果你有留意自行車的示例,你會發現 MountainBike
和 ClassicBike
類非常相似。這就意味著我們不再需要單獨的類了。我們可以建立一個簡單的實現了 Bike
介面的類 GenericBike
,然後山地車和傳統自行車的例項化就像下面這樣:
GenericBike mountainBike = new GenericBike(mbPedalB, mbBackPedalB);
GenericBike classicBike = new GenericBike(cbPedalB, cbBackPedalB);
複製程式碼
我們減少了一半數量的具體自行車類的實現,這意味著我們的程式碼更容易管理。
總結
所有這些原則可能看起來有點矯枉過正,你可能會排斥它們。在很長的一段時間裡,我和你一樣。隨著時間的推移,我開始逐漸把我的程式碼向增強可測試性和更易於維護的方向轉變。漸漸地,我開始這樣來思考事情:“如果只有一種方法可以把兩個部分的內容分開,並將其放在不同的類中,以便我能……”。通常,答案是的確存在這樣的一種方法,並且別人已經實現過了。大多數時候,這種方法都受到 SOLID 原則的啟發。當然,緊迫的工期和其他現實生活中的因素可能會不允許你遵守所有這些原則。雖然很難 100% 實現 SOLID 原則,但是有比沒有強吧。也許你可以嘗試只在那些當需求變更時最容易受影響的部分遵守這些原則。你不必過分遵循它們,可以把這些原則視為你提高程式碼質量的指南。如果你不得不需要製作一個快速原型或者驗證一個概念應用的可行性,那麼你沒有必要盡力去搭一個最佳架構。SOLID更像是一個長期策略,對於必須經得起時間考驗的軟體非常有用。
在這篇由三部分組成的文章中,我試圖給你展示了一些有關 SOLID的我發現比較有趣的東西。關於 SOLID,還有很多看法和解釋,為了更好地理解和從多個角度獲取知識,請多閱讀些其他文章。
我希望這篇文章對你有所幫助。
……
如果你還沒有看過另兩個部分,這裡是它們的連結,第1部分 和 第2部分 。如果你喜歡這篇文章,你可以在我們的 官方站點 上找到更多資訊。