最近看了《Head First Design Patterns》這本書。正如其名,這本書講的是設計模式(Design Patterns),而這本書的第一章,講的是很重要的一些設計原則(Design Principles)。 Identify the aspects of your applicati ...
最近看了《Head First Design Patterns》這本書。正如其名,這本書講的是設計模式(Design Patterns),而這本書的第一章,講的是很重要的一些設計原則(Design Principles)。
Identify the aspects of your application that vary and separate them from what stays the same.(識別應用程式中各個方面的變化,並將它們與保持不變的部分分開。)
Program to an interface, not an implementation.(面向介面而不是實現編程。)
Favor composition over inheritance.(優先考慮組成而不是繼承。)
其中令我感觸頗深的是,“面向介面而不是實現編程”顛覆了我一直以來的認識。
文章中示例代碼為原書中截圖,C#代碼參照文末提供鏈接。
開始
書中用了一個很形象的示例:模擬鴨子程式(SimUDuck)。系統的最初設計使用標準的OO技術,並創建了一個Duck基類,所有其他Duck類型都繼承自該基類。
設計系統時考慮到鴨子都會發出叫聲,而且都會游泳,於是將Quack
方法和Swim
方法定義到Duck
基類中並實現;此外,並不是所有的鴨子都是長得一樣的,那麼將Display
方法在Duck
基類中定義為抽象的,所有繼承自Duck
基類的子類編寫自己的實現。
新的需求產生了!我們需要讓系統中的鴨子可以飛。從面向對象的角度來考慮,如果我們想要代碼重用,只需要在Duck
基類中添加方法Fly
並實現它——所有的鴨子子類都是繼承自Duck
基類的——就實現了讓鴨子飛的功能。我們通過繼承實現了代碼重用,很輕鬆就解決了問題。
也許我們需要深入考慮一下,所有的鴨子都會飛嗎?玩具橡膠鴨呢?我們把Fly
方法的定義及實現放到了Duck
基類中,所有繼承自它的子類都繼承到了Fly
方法,其中也包括了不應繼承Fly
方法的子類。如果按照上面的方案,那我們只能在RubberDuck
橡膠鴨子類中重寫父類的Fly
方法讓RubberDuck
執行Fly
的時候什麼都不做。
再深入一些,如果我們的系統中除了橡膠鴨外,還有其他各種鴨子,比如木頭鴨子呢?這時DecoyDuck
木頭鴨子繼承來的Quack
方法出現了問題——木頭鴨子不會叫!我們只好再把DecoyDuck
中的Quack
方法重寫了......
如果我們改用介面會怎麼樣呢?把Quack
和Fly
方法從基類中拿出來,分別在IQuackable
和IFlyable
介面中定義,然後我們不同的子類根據需要來繼承介面,並實現Quack
或Fly
方法。
當我們有很多個子類鴨子的時候,就要分別為每個繼承了IQuackable
或IFlyable
介面的子類來編寫Quack
或Fly
的實現方法,這完全破壞了代碼重用!值得註意的是,雖然我們在這裡使用了介面,但這並不是面向介面編程。
封裝變化
這裡引入第一條設計原則:Identify the aspects of your application that vary and separate them from what stays the same.(識別應用程式中各個方面的變化,並將它們與保持不變的部分分開。)
換言之:take the parts that vary and encapsulate them, so that later you can alter or extend the parts that vary without affecting those that don’t.(將變化的部分封裝起來,以便以後可以更改或擴展變化的部分而不會影響那些不變的部分。)
這樣帶來的好處是,我們可以進行更少的代碼更改來實現需求功能,減少因代碼更改而帶來的意想不到的影響,並且提高了系統靈活性。
我們知道Duck
的不同子類中,Quack
和Fly
的行為是會發生變化的,那麼我們將Quack
和Fly
方法從Duck
基類中拿出來,併為Quack
和Fly
方法分別創建一些類,來實現各種不同的行為。
面向介面而不是實現編程
設計原則:Program to an interface, not an implementation.(面向介面而不是實現編程。)
在這裡,面向介面而不是實現編程,和封裝變化是相輔相成的。值得註意的是,這裡所說的介面,並不是我們代碼層面上的interface
,"面向介面編程(Program to an interface)所表達的意思實際上是面向基類編程(Program to a supertype),核心思想是利用面向對象編程的多態性。在代碼的具體實現上,我們既可以用Interface
來作為我們所面向的介面,也可以用一個抽象的基類來作為我們面向的介面。遵循面向介面編程,對模擬鴨子程式的Fly
和Quack
行為進行設計,我們可以定義介面IFlyBehavior
、IQuackBehavior
來代表行為Fly
和Quack
,介面的實現則是行為具體的表現形式。我們可以將介面的不同實現類,來賦值給Duck
的不同子類,從而利用繼承及多態來實現面向介面編程。類圖如下:
FlyBehavior
是一個所有不同的Fly
類都要繼承的介面或基類,其中定義了Fly
方法。不同的Fly
類有不同的Fly
方法實現。QuackBehavior
類似。
接下來我們對Duck
類進行更改,將Fly
和Quack
委托出去,不再通過Duck
類或其子類的方法來實現。
首先我們在
Duck
類中定義兩個代表FlyBehavior
和QuackBehavior
的變數。這兩個變數的值是不同的Duck
所需要的特定FlyBehavior
、QuackBehavior
的子類:然後實現
PerformQuack
方法:為
FlyBehavior
和QuackBehavior
賦值:
至此我們就實現了面向介面編程。
我們還可以動態設置Duck
的行為,只需要為Duck
類的FlyBehavior
、QuackBehavior
提供Set
方法(在C#中,使用自動屬性即可)。
優先考慮組成而不是繼承
Favor composition over inheritance.(優先考慮組成而不是繼承。)
HAS-A(有一個)比IS-A(是一個)要好。HAS-A在我們的Duck
系統中可以描述為:每一個Duck
都HAS-A有一個FlyBehavior
,還HAS-A有一個QuackBehavior
,Duck
委托它們來處理Fly
和Quack
的行為。優先考慮組合而不是繼承讓我們的系統擁有更多的靈活性,封裝變化,還可以在運行時動態更改類的行為。