提起面向對象,大家也許覺得自己已經非常“精通”了,起碼也到了“靈活運用”的境界。面向對象設計不就是OOD嗎?不就是用C++、Java、Smalltalk等面向對象語言寫程序嗎?不就是封裝+繼承+多態嗎?
很好!大家已經掌握了不少對面向對象設計的基本要素:開發語言、基本概念、機制。Java是一種純面向對象語言,是不是用Java寫程序就等於面向對象了呢?我先列舉一下面向對象設計的11個原則,測試一下大家對面向對象設計的理解程度~^_^~
- 單一職責原則(The Single Responsibility Principle,簡稱SRP)
- 開放-封閉原則(The Open-Close Principle,簡稱OCP)
- Liskov替換原則(The Liskov Substitution,簡稱LSP)
- 依賴倒置原則(The Dependency Inversion Principle,簡稱DIP)
- 接口隔離原則(The Interface Segregation Principle,簡稱ISP)
- 重用發布等價原則(The Reuse-Release Equivalence Principle,簡稱REP)
- 共同重用原則(The Common Reuse Principle,簡稱CRP)
- 共同封閉原則(The Common Close Principle,簡稱CCP)
- 無環依賴原則(The No-Annulus Dependency Principle,簡稱ADP)
- 穩定依賴原則(The Steady Dependency Principle,簡稱SDP)
- 穩定抽象原則(The Steady Abstract Principle,簡稱SAP)
其中1-5的原則關注所有軟件實體(類、模塊、函數等)的結構和耦合性,這些原則能夠指導我們設計軟件實體和確定軟件實體的相互關系;6-8的原則關注包的內聚性,這些原則能夠指導我們對類組包;9-11的原則關注包的耦合性,這些原則幫助我們確定包之間的相互關系。
1 單一職責原則(SRP)
就一個類而言,應該僅有一個引起它變化的原因。
在SRP中,我們把職責定義為“變化的原因”。如果你能夠想到多於一個動機去改變一個類,那么這個類就具有多於一個的職責。有時,我們很難注意到這一點,我們習慣於以組的形式去考慮職責。
1.1 Rectangle類
例如,圖2.1-1,Rectangle類具有兩個方法,一個方法把矩形繪制在屏幕上,另一個方法計算矩形面積。

圖2.1-1 多於一個的職責
有兩個不同的應用程序使用Rectangle類。一個是有關計算幾何學方面的,Rectangle類會在幾何形狀計算方面為它提供幫助,它從來不會在屏 幕上繪制矩形。另一個應用程序是有關圖形繪制方面的,它可能進行一些幾何學方面的工作,但是它肯定會在屏幕上繪制矩形。
這個設計違反了SRP。Rectangle類具有兩個職責。第一個職責提供了矩形幾何形狀數學模型;第二個職責是把矩形在一個圖形用戶界面上繪制出來。
對於SRP的違反導致了一些嚴重的問題。首先,我們必須在計算幾何應用程序中包含GUI代碼。如果這是一個C++程序,就必須要把GUI代碼鏈接進來,這會浪費鏈接時間、編譯時間以及內存占用。如果是一個JAVA程序,GUI的.class文件必須要部署到目標平台。
其次,如果Graphical Application的改變由於一些原因導致了Rectangle的改變,那么這個改變會迫使我們重新構建、測試已經部署Computational Geometry Application。如果忘記了這樣作,Computational Geometry Application可能會以不可預測的方式失敗。
一個較好的設計是把這兩個職責分離到圖2.1-2中所示的兩個完全不同的類中。這 個設計把Rectangle類中進行計算的部分移到GeometryRectangle類中,現在矩形繪制方式 的改變不會對Computational Geometry Application造成影響。

圖2.1-2 分離的職責
1.2 結論
SRP是所有原則中最簡單的原則之一,也是最難正確運用的原則之一。我們會自然地把職責結合在一起。軟件設計真正要做到的許多內容,就是發現職責,並把那些職責相互分離。事實上,我們要論述的其余原則都會以這樣或那樣的方式回到這個問題上。
2 開放-封閉原則(OCP)
軟件實體(類、模塊、函數等)應該是可以擴展的,但是不可修改的。
遵循OCP設計出的模塊具有兩個主要的特征:
1、 對於擴展是開放的(Open for extension)
這意味着模塊的行為是可以擴展的。當應用的需求變化時,我們可以對模塊進行擴展,使其具有滿足那些改變的新行為。換句話說,我們可以改變模塊的功能。
2、 對於更改是封閉的(Closed for modification)
對模塊行為進行擴展時,不必改動模塊的源代碼或者二進制代碼。模塊的二進制可執行版本,無論是共享庫、dll或者Java的jar文件,都無需改動。
這兩個特征好像是相互矛盾的。擴展模塊行為的通常方式就是修改模塊的源代碼。不允許修改的模塊常常都被認為是具有固定的行為。怎樣可能在不改動模塊源代碼的情況下去更改它的行為呢?怎樣才能在無需對模塊進行改動的情況下就改變它的功能呢?——關鍵是抽象!
2.1 Shape應用程序
我們有一個需要在標准GUI上繪制圓和正方形的應用程序。
2.1.1 違反OCP
程序2.2.1.1-1 Square/Circle問題的過程化解決方案
------------------------------shape.h------------------------------
enum ShapeType {circle, square };
struct Shape
{
ShapeType itsType;
}
------------------------------circle.h------------------------------
#include shape.h
struct Circle
{
ShapeType itsType;
double itsRadius;
Point itsCenter;
};
------------------------------square.h------------------------------
#include shape.h
struct Aquare
{
ShapeType itsType;
double itsSide;
Point itsTopLeft;
};
------------------------------drawAllShapes.c------------------------------
#include shape.h
#include circle.h
#include square.h
typedef struct Shape* ShapePointer;
Void DrawAllShapes(ShapePointer list[], int n)
{
int i;
for (i = 0; i < n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*) s );
Break;
case circle:
DrawCircle((struct Circle*) s );
Break;
}
}
}
DrawAllShapes函數不符合OCP,因為它對於新的形狀類型的添加不是封閉的。如果希望這個函數能夠繪制包含有三角形的列表,就必須更改這個函數。事實上每增加一種新的形狀類型,都必須要更改這個函數。
同樣,在進行上述改動時,我們必須要在ShapeType enum中添加一個新的成員。由於所有不同種類的形狀都依賴於這個enum的聲明,所有我們必須要重新編譯所有的形狀模塊。並且也必須要重新編譯所有依賴於Shape類的模塊。
程序2.2.1.1-1中的解決方案是僵化的,這是因為增加Triangle會導致Shape、Square、Circle以及 DrawAllShapes的重新編譯和重新部署。該方法是脆弱的,因為很可能在程序的其他地方也存在類似的既難以查找又難以理解的 switch/case或者if/else語句。該方法是牢固的,因為想在另一個程序中復用DrawAllShapes時,都必須附帶上Square和 Circle,即使那個新程序不需要它們。因此該程序展示了許多糟糕設計的臭味。
2.1.2 遵循OCP
程序2.2.1.2-1 Square/Circle問題的OOD解決方案
class Shape
{
public:
virtual void Draw() const = 0;
};
class Square : public Shape
{
public:
virtual void Draw() const;
};
class Circle : public Shape
{
public:
virtual void Draw() const;
};
void DrawAllShapes(vector<Shape*>& list)
{
vector<Shape*>::iterator i;
for (i == list.begin(); i != list.end(); i++)
(*i)->Draw();
}
可以看到,如果我們要擴展程序2.2.1.2-1中 DrawAllShapes函數的行為,使之能夠繪制一種新的形狀,我們只需增加一個新的Shape派生類。DrawAllShapes函數並不需要改 動。這樣DrawAllShapes就符合了OCP。無需改動自身的代碼就可以擴展它的行為。實際上,增加一個Triangle類對於這里展示的任何模塊 完全沒有影響。很明顯,為了能夠處理Triangle類,必須改動系統中的某些部分,但是這里展示的所有代碼都無需改動。
這個程序是符合OCP的。對它的改動是通過增加新代碼進行的,而不是更改現有的代碼。因此,它就不會引起像不遵循OCP的程序那樣的連鎖改動。所需要的改動僅僅是增加新的模塊,以及為了能夠實例化新類型的對象而進行的圍繞main的改動。
2.1.3 是的,我說謊了
上面的例子其實並非是100%封閉的!如果我們要求所有的圓必須在正方形之前繪制,那么程序2.2.1.2-1中DrawAllShapes函數無法對這種變化做到封閉。
這就導致一個麻煩的結果,一般而言,無論模塊是多么的封閉,都會存在一些無法對之封閉的變化。沒有對於所有的情況都貼切的模型。
既然不可能完全封閉,那么就必須有策略地對待這個問題。也就是說,設計人員必須對於他設計的模塊應該對哪種變化封閉作出選擇。他必須先猜測出最有可能發生的變化種類,然后構造抽象來隔離那些變化。
有句古老的諺語說:“愚弄我一次,應感羞愧的是你。再次愚弄我,應感羞愧的是我。”這也是一種有效的對待軟件設計的態度。為了防止軟件背負着不必要的復 雜性 ,我們會允許自己被愚弄一次。這意味着在我們最初編寫代碼時,假設變化不會發生。當變化發生時,我們就創建抽象來隔離以后發生的同類變化。簡而言之,我們 願意被第一顆子彈擊中,然后我們會確保自己不再被同一支槍發射的其他任何子彈擊中。
2.2 結論
在許多方面,OCP是面向對象設計的核心所在。遵循這個原則可以帶來面向對象技術所聲稱的巨大好處(也就是:靈活性、可重用性以及可維護性)。然而,並 不是說只要使用一種面向對象語言就是遵循了這個原則。對於應用程序中的每個部分都肆意地進行抽象同樣不是一個好主意。正確的做法是,開發人員應該僅僅對程 序中呈現出頻繁變化的那些部分做出抽象。拒絕不成熟的抽象和抽象本身一樣重要。
3 Liskov替換原則(LSP)
子類型(subtype)必須能夠替換掉它們的基類型(base type)。
Barbara Liskov首次寫下這個原則是在1988年。她說道:
這里需要如下替換性質:若對每個類型S的對象o1,都存在一個類型T的對象o2,使得在所有針對T編寫的程序P中,o1替換o2后,程序P行為和功能不變,則S是T的子類型。
想想違反該原則的后果,LSP的重要性就不言而喻了。假設有一個函數f,它的參數為指向某個基類型B的指針或引用。同樣假設某個B的派生類D,如果把D的對象作為B類型傳遞給f,會導致f出現錯誤行為。那么D就違反了LSP。顯然D對f來說是脆弱的。
f的編寫者會想去對D進行一些測試,以便於在把D的對象傳遞給f時,可以使f具有正確的行為。這個測試違反了OCP,因為此f對於B的所有不同的派生類 都不再是封閉的。這樣的測試是一種代碼的臭味,它是缺乏經驗的開發人員(或者,更糟的,匆忙的開發人員)在違反了LSP時所產生的結果。
3.1 正方形和矩形,微妙的違規
程序2.3.1-1 Rectangle類和Square類
class Rectangle
{
public:
void SetWidth(double w) {itsWidth = w;}
void SetHeight(double h) {itsHeight = h;}
double GetWidth() {return itsWidth;}
double GetHeight() {return itsHeight;}
private:
Point itsTopLeft;
double itsWidth;
double itsHeight;
};
class Square : public Rectangle
{
public:
void SetWidth(double w)
{
Rectangle::SetWidth(w);
Rectangle::SetHeight(w);
}
void SetHeight(double h)
{
Rectangle::SetWidth(h);
Rectangle::SetHeight(h);
}
};
從一般意義上講一個正方形就是一個矩形。因此,把Square類視為從Rectangle類派生是合乎邏輯的。
IS-A關系的這種用法有時被認為是面向對象分析(OOA)的基本技術之一。一個正方形是一個矩形,所以Square類就派生自Rectangle類。不 過這個想法會帶來一些微妙但極為嚴重的問題。一般來說,這些問題是難以預見的,直到我們編寫代碼時才會發現它們。
我們 首先注意到出問題的地方是,Square類並不同時需要成員變量itsHeight和itsWidth。但是Square類仍會在Rectangle類中 繼承它們。顯然這是個浪費。在許多情況下,這種浪費是無關緊要的。但是,如果我們必須創建成百上千個Square對象,浪費的程度則是巨大的。
假設目前我們並不十分關心內存效率。從Rectangle類派生Square類也會產生其他一些問題。請考慮下面這個函數:
void f (Rectangle& r)
{
r.SetWidth(32); // Calls Rectangle::SetWideth()
}
如果我們向這個函數傳遞一個指向Square對象的引用,這個Square對象就會被破壞,因為他們的長並不會改變。這顯然違反了LSP。以 Rectangle派生類的對象作為參數傳入是,函數f不能正確運行。錯誤的原因是在Rectangle中沒有把SetWidth和SetHeight聲 明為虛函數,因此它們不是多態的。
這個錯誤很容易修正。然而,如果派生類的創建會導致我們改變基類,這就常常意味着設 計是有缺陷的。當然也違反了OCP。也許有人會反駁說,真正的設計缺陷是忘記把SetWidth和SetHeight聲明為虛函數,而我們已經作了修正。 可是,這很難讓人信服,因為設置一個長方形的長和寬是非常基本的操作。如果不是預見到Square類的存在,我們憑什么要把這兩個函數聲明為虛函數呢?
盡管如此,假設我們接受這個理由並修正這些類。
程序2.3.1-2 修正后的Rectangle類
class Rectangle
{
public:
virtual void SetWidth(double w) {itsWidth = w;}
virtual void SetHeight(double h) {itsHeight = h;}
double GetWidth() {return itsWidth;}
double GetHeight() {return itsHeight;}
private:
Point itsTopLeft;
Double itsWidth;
Double itsHeight;
};
3.1.1 真正的問題
現在Square和Rectangle看起來都能夠正常工作。無論Square對象進行什么樣的操作,它都和數學意義上的正方形保持一致。無論 Rectangle對象進行什么樣的操作,它都和數學意義上的長方形保持一致。此外,可以向接受指向Rectangle的指針或引用的函數傳遞 Square,而Square依然保持正方形的特性,與數學意義上的正方形一致。
這樣看來,設計似乎是自相容的、正確的。可是,這個結論是錯誤的。一個自相容的設計未必就和所有的用戶程序自相容。考慮下面的函數g:
void g (Rectangle& r)
{
r.SetWidth(5);
r.Setheight(4);
assert(r.Area() == 20);
}
這個函數認為所傳遞進來的一定是Rectangle,並調用了其成員函數SetWidth和SetHeight。對於Rectangle來說,此函數運 行正確,但如果傳遞進來的是Square對象就發生斷言錯誤(assertion error)。所以,真正的問題是:函數g的編寫者假設改變Rectangle的寬不會導致其長的改變。
很顯然,改變 一個長方形的寬不會影響它的長的假設是合理的!然而,並不是所有可以作為Rectangle傳遞的對象都滿足這個假設。如果把一個Square類的實例傳 遞給g這樣做了假設的函數,那么這個函數就會出現錯誤的行為。函數g對於Square/Rectangle層次結構來說是脆弱的。
函數g的表現說明有一些使用指向Rectangle對象的指針或者引用的函數,不能正確地操作Square對象。對於這些函數來說,Square不能替換Rectangle,因此Square和Rectangle之間的關系是違反LSP的。
3.1.2 IS-A是關於行為的
那么究竟是怎么會使?Square和Rectangle這個顯然合理的模型為什么會有問題呢?畢竟,Square應該就是Rectangle。難道他們之間不存在IS-A關系嗎?
對於那些不是g的編寫這而言,正方形可以是長方形,但是從g的角度來看,Square對象絕對不是Rectangle對象。為什么!?因為Square 對象的行為方式和函數g所期望的Rectangle對象的行為方式不相容。從行為方式的角度來看,Square不是Rectangle,對象的行為方式才 是軟件真正所關注的問題。LSP清楚地指出,OOD中IS-A關系是就行為方式而言的,行為方式是可以進行合理假設的,是客戶程序所依賴的。
3.2 從派生類中拋出異常
另一種LSP的違規形式是在派生類的方法中添加了其他基類不會拋出的異常。如果基類的使用者不期望這些異常,那么把它們添加到派生類的方法中就會導致不可替換性。此時要遵循LSP,要么就必須改變使用者的期望,要么派生類就不應該拋出這些異常。
3.3 有效性並非本質屬性
在考慮一個特定設計是否恰當時,不能完全孤立地來看這個解決方案。必須要根據設計的使用者做出的合理假設來審視它。
有誰知道設計的使用者會做出什么樣的合理假設呢?大多數這樣的假設都很難預測。事實上,如果試圖去預測所有這些假設,我們所得到的系統很可能會充滿不必要的復雜性的臭味。因此,像所有其他原則一樣,通常最好的方法只預測那些最明顯的對於LSP的違反情況而推遲所有其他的預測,知道出現相關的脆弱性的臭味時,才去處理它們。
3.4 結論
OCP是OOD中很多說法的核心。如果這個原則應用得有效,應用程序就會具有更多的可維護性、可重用性以及健壯性。LSP是使OCP成為可能的主要原則之一。正是子類型的可替換性才使得使用基類類型的模塊在無需修改的情況下就可以擴展。這種可替換性必須使開發人員可以隱式依賴的東西。因此,如果沒有顯式地強制基類類型的契約,那么代碼就必須良好地並且明顯地表達出這一點。
俗語“IS-A”的含義過於寬泛以至於不能作為子類型的定義。子類型的正確定義是“可替換性的”,這里的可替換性可以通過顯式或隱式的契約來定義。
4 依賴倒置原則(DIP)
A、 高層模塊不應該依賴於低層模塊,兩者都應該依賴於抽象。
B、 抽象不應該依賴於細節,細節應該依賴於抽象。
這條原則的名字中使用“倒置”這個詞,是由於許多傳統的軟件開發方法,例如結構化分析和設計,總是傾向於創建一些高層模塊依賴於低層模塊,策略 (policy)依賴於細節的軟件結構。實際上這些方法的目的之一就是要定義子程序層次結構,該層次結構描述了高層模塊怎樣調用低層模塊。第一章中1.2 節的Copy程序的初始設計就是這種層次結構的一個典型示例。一個設計良好的面向對象的程序,其依賴程序結構相對於傳統的過程式方法設計的通常結構而言就 是被“倒置”了。
請考慮一下高層模塊依賴於低層模塊時意味着什么。高層模塊包含了一個應用程序的重要的策略選擇和業務 模型。正是這些高層模塊才使得其所在的應用程序區別於其他。然而,如果這些高層模塊依賴於低層模塊,那么對低層模塊的改動就會直接影響到高層模塊,從而迫 使它們依次做出改動。
這種情形是非常荒謬的!本應該是高層的策略設置模塊去影響低層的細節實現模塊的。包含業務規則的模塊應該優先於並獨立於包含實現細節的模塊。無論如何高層模塊都不應該依賴於低層模塊。
此外,我們更希望能夠重用的是高層的策略設置模塊。我們已經非常擅長於通過子程序庫的形式來重用低層模塊。如果高層模塊依賴於低層模塊,那么在不同的上 下文中重用高層模塊就會變得非常困難。然而,如果高層模塊獨立於低層模塊,那么高層模塊就可以非常容易的被重用。該原則是框架(framework)設計 的核心原則。
4.1 層次化
請看圖2.4.1-1的層次化方案:

圖2.4.1-1 簡單的層次化方案
圖中,高層的Policy Layer使用了低層的Mechanism Layer,而Mechanism Layer又使用了更細節的層Utility Layer。這看起來似乎是正確的,然而它存在一個隱伏的錯誤特征,那就是:Policy Layer對於其下一直到Utility Layer的改動都是敏感的。這種依賴關系是傳遞的。Policy Layer依賴於某些依賴於Utility Layer的層次;因此Policy Layer傳遞性的依賴於Utility Layer。這是非常糟糕的。
圖 2.4.1-2展示了一個更為適合的模型。每個較高層次都為它所需的服務聲明一個抽象接口,較低的層次實現了這個抽象接口,每個高層類都通過該抽象接口使 用下一層,這樣,高層就不依賴於低層。低層反而依賴於在高層中聲明的抽象服務接口。這不僅解除了Policy Layer對於Utility Layer的傳遞依賴關系,甚至也解除了Policy Layer對Mechanism Layer的依賴關系。

圖2.4.1-2 倒置的層次
請注意這里的倒置不僅僅是依賴關系的倒置,它也是接口所有權的倒置。我們通常會認為工具庫應該擁有它們自己的接口。但是當應用了DIP時,我們發現,往往是客戶端擁有抽象接口,而它們的服務者這從這些抽象接口派生。
4.1.1 倒置接口所有權
這就是著名的Hollywood原則:“Don’t call us, we’ll call you.”(不要調用我們,我們會調用你。)低層模塊實現了在高層模塊中聲明並被高層模塊調用的接口。
通過倒置接口所有權,對於Mechanism Layer或者Utility Layer的任何改動都不會在影響到Policy Layer。而且,Policy Layer可以在實現了Policy Service Interface的任何上下文中重用。這樣,通過倒置這些依賴關系,我們創建了一個更靈活、更持久、更易改變的結構。
4.1.2 依賴於抽象
一個稍微簡單但仍然非常有效的對於DIP的解釋,是這樣一個簡單的啟發式規則:“依賴於抽象 ”。這是一個簡單的陳述,該啟發式規則建議不應該依賴於具體類——也就是說,程序中所有的依賴關系都應該終止於抽象類或者接口。
根據啟發式規則:
- 任何變量都不應該持有一個指向具體類的指針或者引用
- 任何類都不應該從具體類派生
- 任何方法都不應該覆寫它的任何基類中已經實現了的方法
當然,每個程序都會有違反該規則的情況。有時必須要創建具體類的實例,而創建這些實例的模塊將會依賴於它們。此外,該啟發規則對於那些雖然是具體但卻穩定(nonvolatile)的類來說似乎不太合理。如果一個具體類不太會改變,並且也不會創建其他類似的派生類,那么依賴於它並不會造成損害。
例如,在大多數系統中,描述字符串的類都是具體的(如Java中的String類),而該類有時穩定的,也就是說,它不太會改變。因此,直接依賴於它不會造成損害。
然而,我們在應用程序中所編寫的大多數具體類都是不穩定的。我們不想直接依賴於這些不穩定的具體類。通過把它們隱藏在抽象接口的后面,可以隔離它們的不穩定性。
這不是一個完美的解決方案。常常,如果不穩定類的接口必須變化時,這個變化一定會影響到該類的抽象接口。這種變化破壞了抽象接口維系的隔離性。
由此可知,該啟發規則對問題的考慮有點簡單了。另一方面,如果看得遠一點,認為是由客戶來聲明它需要的服務接口,那么僅當客戶需要時才會對接口進行改變。這樣,改變實現抽象接口的類就不會影響到客戶。
4.2 結論
使用傳統的過程化程序設計所創建出來的依賴關系結構,策略是依賴於細節的。這是糟糕的,因為這樣會使策略受到細節改變的影響。面向對象的程序設計倒置了依賴關系結構,使得細節和策略都依賴於抽象,並且常常是客戶擁有服務接口。
事實上,這種依賴關系的倒置正好是面向對象設計的標志所在。使用何種語言來編寫程序是無關緊要的。如果程序的依賴關系是倒置的,它就是面向對象的設計。否則,它就是過程化的設計。
DIP是實現許多面向對象技術所宣稱的好處的基本低層機制。它的正確應用對於實現可重用的框架來說是必須的。同時它對構建在變化面前富有彈性的代碼也是非常重要的。由於抽象和細節彼此隔離,所以代碼也非常容易維護。
5 接口隔離原則(ISP)
不應該強迫客戶依賴於它們不要的方法。接口屬於客戶,不屬於它所在的類層次結構。
這個原則用來處理“胖”接口所具有的缺點。如果類的接口不是內聚的(cohesive),
就表示該類具有“胖”接口。換句話說,類的“胖”接口可以分解成多組方法。每一組方法都服務於一組不同的客戶程序。這樣,一些客戶程序可以使用一組成員函數,而其他客戶程序可以使用其他組的成員函數。
ISP承認存在有一些對象,它們確實不需要內聚的接口:但是ISP建議客戶程序不應該看到它們作為單一的類存在。相反,客戶程序看到的應該是多個具有內聚接口的抽象基類。
如果強迫客戶程序依賴於那些它們不使用的方法,那么這些客戶程序就面臨着由於這些沒使用的方法的改變所帶來的變更。這無意中導致了所有客戶程序之間的耦 合。換種說法,如果一個客戶程序依賴於一個含有它不使用的方法的類,但是其他客戶程序卻要使用該方法,那么當其他客戶要求這個類改變時,就會影響到這個客 戶程序。我們希望盡可能地避免這種耦合,因此我們希望分離接口。
5.1 ATM用戶界面的例子
現在我們考慮一下這樣一個例子:傳統的自動取款機(ATM)問題。ATM需要一個非常靈活的用戶界面。它的輸出信息需要被轉換成許多不同的語言。輸出信 息可能被顯示在屏幕上,或者布萊葉盲文書寫板上,或者通過語音合成器說出來。顯然,通過創建一個抽象基類,其中具有用來處理所有不同的、需要被該界面呈現 的消息的抽象方法,就可以實現這種需求。如圖2.5.1-1所示:

圖2.5.1-1 ATM界面層次結構
同樣,可以把每個ATM可以執行的不同操作封裝為類Transaction的派生類。這樣,我們可以得到類DepositTransaction、 WithdrawalTransaction以及TransferTransaction。每個類都調用UI的方法。例如,為了要求用戶輸入希望存儲的金 額,DepositTransaction對象會調用UI類中的RequestDepositAmount方法。同樣,為了要求用戶輸入想要轉帳的金 額,TransferTransaction對象會調用UI類中的RequestTransferAmount方法。圖2.5.1-2為相應的類圖。

圖2.5.1-2 ATM操作層次結構
請注意,這正好是ISP告訴我們應該避免的情形。每個操作所使用的UI的方法,其他的操作類都不會使用。這樣,對於任何一個Transaction的派 生類的改動都會迫使對UI的相應改動,從而也影響了其他所有Transaction的派生類以及其他所有依賴於UI接口的類。這樣的設計就具有了僵化性以 及脆弱性的臭味。
例如,如果要增加一種操作PayGasBillTransaction,為了處理該操作想要顯示的特 定消息,就必須在UI中加入新的方法,糟糕的是,由於DepositTransaction、WithdrawalTransaction以及 TransferTransaction全部都依賴於UI接口,所以它們都需要重新編譯。更糟糕的是,如果這些操作都作為不同的DLL或者共享庫部署的 話,那么這些組件必須得重新部署,即使它們的邏輯沒有做過任何改動。你聞到粘滯性的臭味了嗎?
通過將UI接口分解成像DepositUI、WithdrawalUI以及TransferUI這樣的單獨接口,可以避免這種不合適的耦合。最終的UI接口可以去多重繼承這些單獨的接口。圖2.5.1-3展示了這個模型。

圖2.5.1-3 分離的ATM UI接口
每次創建一個Transaction類的新派生類時,抽象接口UI就需要增加一個相應的基類並且因此UI接口以及所有他的派生類都必須改變。不過,這些 類並沒有被廣泛使用。事實上,它們可能僅被main或者那些啟動系統並創建具體UI實例之類的過程所使用。因此,增加新的UI基類所帶來的影響被減至最 小。
5.2 結論
胖類(fat class)會導致它們的客戶程序之間產生不正常的並且有害的耦合關系。當一個客戶程序要求該胖類進行一個改動時,會影響到所有其他的客戶程序。因此,客 戶程序應該僅僅依賴於它們實際調用的方法。通過把胖類的接口分解成多個特定於客戶程序的接口,可以實現這個目標。每個特定於客戶程序的接口僅僅聲明它的特 定客戶或者客戶組調用的那些函數。接着,該胖類就可以繼承所有特定於客戶程序的接口,並實現它們。這就解決了客戶程序和它們沒有調用的方法間的依賴關系, 並使客戶程序之間互不依賴。
