老話重彈——再談接口與抽象類
1. 從依賴倒置說起
首先,我們來看下《敏捷軟件開發》中對依賴倒置的說明:
a. 高層模塊不應該依賴于低層模塊,二者都應該依賴于抽象。
b. 抽象不應該依賴于細節,細節應該依賴于抽象。
我們先拋開第二點來看第一點,什么叫高層模塊,什么叫低層模塊。在我理解來看:高層模塊也就是戰略性模塊,業務性模塊。而低層模塊就是戰術性模塊,細節類模塊。
先來看這樣一段代碼:
{
private Mouth mouth;
public Person(Mouth mouth)
{
this.mouth = mouth;
}
/// <summary>
/// 吃飯
/// </summary>
public void Eat()
{
if (mouth == null)
{
throw new NullReferenceException();
}
mouth.OpenMouth();
FillMouthWithFood();
mouth.CloseMouth();
}
private void FillMouthWithFood(){ }
}
class Mouth
{
/// <summary>
/// 張嘴
/// </summary>
public void OpenMouth() { }
/// <summary>
/// 閉嘴
/// </summary>
public void CloseMouth() { }
}
也許有人會說,這是再正常不過的代碼了。但是我們要考慮到,張嘴和閉嘴的動作是具體的行為,具體的行為就可能發生變化。而在這里,高層代碼Person類還依賴于低層代碼,這時當低層代碼發生變化時,高層代碼也就會隨之發生變化。而如果隨著層數的增多,震蕩地劇烈程度也就會隨之增加。
接下來我們來看這樣一個圖:
在傳統的過程式設計中,復用都偏向于低層次模塊的復用,例如說算法的復用,數據結構的復用,再高級一些可能是某些通用函數的復用,是細節的重用,而忽略了戰術層次上的重用。這就是高層依賴于底層的缺點。(請注意,我并沒有說,這樣不好。)
那么,我們要怎么樣來解決這種情況。
2. 抽象,我們談談抽象
這個詞太時尚了,當然,這個詞本身也很抽象,那么什么叫做抽象。
我們來看看《現代漢語詞典》對這個詞的解釋:抽象是從許多事物中,舍棄個別的,非本質的屬性,抽出共同的特征而形成的。
那么說,抽象類和接口都屬于抽象。所以很多書上說:接口優于抽象類,我覺得這句話實在是一句非常扯淡的話!接口是抽象,難道抽象類就不是抽象了?明明是兩種完全不同的東西,談何比較?!
面試的時候,大家也許都被面試過一個問題就是接口和抽象類的區別,網上最常見的一種答案就是:接口代表一種Can-Do語義,抽象類是一個Is-a的語義。這句話不無道理,我以前也都是這樣來回答筆試和面試題。可是現在我覺得這句話并不能對真正的設計有任何的幫助。
這么說吧,任何方法都是可以被表示成Can-Do的語義,那是不是說,所有的方法都要被放到接口中,而所有的抽象類都是貧血類,或者說所有的抽象類都要實現接口么?
3. 接口和抽象類的區別
回顧我們在第一點種說的依賴反轉原則,高層不應該依賴于低層,那么也就是說在設計時,不應該遵循從低層到高層的設計。而應該先設計整體的業務(也就是高層模塊,戰略性內容),然后再根據高層模塊去設計低層模塊(也就是具體實現)。而在高層與底層之前也不應該直接產生依賴,而應該在他們兩層之間搭建一層抽象,這層抽象在我看來可以說叫做“接口”。
那么這里我就提出我對接口和抽象類的認識:接口是從高層需求而來,抽象類是從底層總結而來。
我來解釋一下我這句話,在設計一個高層模塊的時候,這個高層模塊不知道低層模塊的細節,甚至不知道實現方式。它只知道我需要一個這個行為,然后需要一個這個行為。比如這個代碼:
這是一個高層代碼,比賽中有一項運動是先跑步,在競走,最后是游泳。也許比賽分為男子組和女子組,不同的人走路,跑步方式還不一樣。但是我并不關心這個行為的所屬者是誰,我只要求知道,我需要這個行為。這個就是接口的來源。class Game
{
public void Sport()
{
Run();
Walk();
Swim();
}
}而抽象類則是從底層的實現中提煉出來的。所以我們不妨也可以換種說法。“接口是一個面向對象分析與設計的必然結果,而抽象類則是重構的結果”。或者我們也可以這樣說:“先有接口,后有抽象類”。class Game
{
private IRunable runner;
private IWalkable walker;
private ISwimable swimmer;
public Game(IRunable runner,IWalkable walker, ISwimable swimmer)
{
this.runner = runner;
this.walker = walker;
this.swimmer = swimmer;
}
public void Sport()
{
runner.Run();
walker.Walk();
swimmer.Swim();
}
}
interface IRunable
{
void Run();
}
interface IWalkable
{
void Walk();
}
interface ISwimable
{
void Swim();
}本來想結束這一節,但是為了希望大家進一步理解我的觀點,我再補一句,絕對不應該從兩個類中抽取出接口。
4.自底向上vs.自頂向下
究竟是自底向上還是自頂向下,這兩種究竟哪一種更好,相信每個人都有自己的觀點。那么我們來想一下這兩種方法的優劣。
自底向上方法關鍵在于組裝,先設計低層,低層不知道高層的業務,就像搭積木,每個積木都不是為了最終的建筑物而設計,而是遵循它自有的形狀。關鍵在于玩家自己的組裝。
自頂向下方法的關鍵在于最初業務的分析,根據需求文檔中的業務,提取出業務模型,然后根據業務模型分析出每個業務的行為,然后去分別實現每個行為。
自底向上方法的最大優點在于底層模塊的重用性,就像積木一樣,可以搭成高樓,也可以改成狗屋。但是劣勢在于底層的變動會引起高層的級聯震蕩,此外,積木設計者相當于我們的架構師,玩家相當于我們“套頁面”的程序員,架構師的好壞會完全直接影響到最終的項目成敗。
自頂向下方法最大的優點就在于上面提的,依賴倒置會保證業務的可重用性,此外,不會讓架構師去憑空想象一個業務邏輯,而是根據需求文檔中的業務邏輯去整個他的業務模型。當然,劣勢就在于底層代碼的重用性會極低。
那么,我們究竟該如何去做呢。這里我只提出自己的觀點。
在軟件工程中有一個“V”字型模型,具體是做什么的我有些記不清楚了。在這里,我覺得也可以用V字型模型來做。
在接到需求文檔的時候,采用自頂向下的分析方法來整理出相關業務,然后每個業務的大致實現邏輯,根據這些實現邏輯來抽取去接口,OK。高層模塊已經設計完了。
接下來,我們來采用自底向下的方法來分析。其實就是我們常說的“領域模型”,相信我,單純地用自頂向下的分析方法是很難設計出通用的領域模型的。這個領域模型,就是我們最可重用的模塊。而領域模型的設計,也正是我們“設計模式”的最大應用之處。
看到上面的兩段,就是先有接口后有抽象類的典型。
說到最后,越說越亂了,算了,停下來吧。。。。。。