嗚嗚喔學習筆記: 好文分享 你搞懂抽象類別別與介面了嗎?

搜尋此網誌

2016年12月6日 星期二

好文分享 你搞懂抽象類別別與介面了嗎?

我一直搞不清楚 Interface abstract 的差異 跟使用的方向 偶然看到一篇好文在此分享一個

我為下面文章做個總結:

Abastract => 是一個 => 皮卡丘 是一個神奇寶貝
Interface => 像是一個 , 屬於 => 皮卡丘 像是一個雷系 or 屬於雷系

class Abstract 神奇寶貝 { ... }
Interface 雷系 { ... }

以這樣來分類結論就是

class 皮卡丘 : 神奇寶貝 , 雷系 { ... }
---------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------

原文連結: http://missrices.pixnet.net/blog/post/28220534#comment-28568035

若你真的需要繼承某class,但又希望別人無法ne它,就將它宣告成abstract;
若你沒有需要繼承某class,只希望N個class都要求有某些功能,就先宣告一個interface,讓各class去實作它!


你搞懂抽象類別別與介面了嗎?


引用自:.Net Go2 OO
你搞懂抽象類別與介面了嗎?(一)
http://netgo2oo.blog.ithome.com.tw/trackbacks/224/779
你搞懂抽象類別與介面了嗎?(二) 
http://blog.ithome.com.tw/index.php?op=ViewArticle&articleId=805&blogId=224


你搞懂抽象類別與介面了嗎?(一) 


在物件導向設計中,抽象類別別與界面的相似性往往造成了設計師的困擾,進而在使用上產生混淆,本篇文章希望能夠透過幾個不同的觀點來描述這兩之間的差異性。




概念上的差異


我們先回到物件導向中關於類別繼承的基本解釋,子類別別與父類別別(註一)之間的關係可以說是一種「Is A」的關係。聽過的會覺得很正常,沒聽過的就會丈二金剛摸不著頭腦。


先舉一個簡單的例子,假設有一個叫做車子的類別,另外有一個叫做卡車的類別。如果卡車是繼承了車子類別別而成為車子的子類別別,我們可以就說卡車「Is A」車子,也就是說卡車是車子的一種。這樣講應該很容易理解。


那麼抽象類別別呢(Abstract Class)?原則上最簡單的解釋是,抽象類別別便是在領域概念中無法具體化的類別。


這樣講似乎還是很籠統,我們再舉一個簡單的例子。假設有一個叫做「形狀」的類別,而「圓形」類別與「方形」類別則是「形狀」類別的子類別別。在這個例子中,「形狀」很明顯的是一種抽像的概念,並無法具體化,因此在設計時,我們便可以選擇將「形狀」定義為一個抽象類別別。


由於「圓形」是「形狀」的子類別別,因此「圓形」具備了「形狀」的一切能力,如果是在繪圖軟體中,「圓形」便可由「形狀」中繼承到拉近的能力。在「形狀」中,設計師便可以先將拉近的能力細節將以描述清楚,「圓形」則可直接使用。


回過頭來,我們來思考界面(Interface)的用處。在概念上,我們可以說界面是一種可以附上在任意類別上的特徵,也是一種規範。


延伸上面的例子,假設「形狀」本身且不具備儲存的能力,我們想幫「圓形」加上這個能力,但是又不想「方形」具備儲存的能力。在這個狀況裡,在父類別別「形狀」中加上儲存的能力是不可行的,因為並不是所有圖形都需要可以儲存。


為了解決這個難題,我們最直覺的方式是在「圓形」中直接加上儲存的能力。理論上這樣是可行的,延伸思考是,如果還有其它的類別,例如「三角形」,也要加上這個能力時,會不會因為設計師的不同而讓兩個類別中的儲存的能力出現不一致,其實做的卻是同一件事情的狀況?這樣狀況就會有些失控了。


此外,假設儲存這個能力是需要與一個物件「大媽」(名字隨便取,不要想太多)相配合關連的,而該「大媽」不可能滿足所有類別實作儲存時的千奇百怪的設計方式,只能滿足一種。


那該怎麼辦才好呢?前面筆者提過了,界面也可以說是一種「規範」。


如果我們將儲存的能力定義在儲存的界面中,任何類別需要增加儲存能力時,只要遵循儲存界面的定義,那麼既可完成這個需求,又可以達到一致性。反過來說,在負責儲存的「大媽」中,他也只需要針對儲存界面來完成他的工作,他並不需要瞭解到底究竟有幾百個類別需要用到他來完成儲存這個功能。


如此豈不完美?


以上筆者用概念上的解釋來說明界面與抽象類別別在用途上的差異,下一篇(註二),我們將來研究如在實作上理解兩者的差異。


註:


一、父類別別是我的Partner建議的,我本來是想用母類別,我比較敬愛母親。


二、下一篇將由我的Partner負責執筆,敬請期待。




你搞懂抽象類別別與介面了嗎?(二) 

延續著上一篇所著重的是在於介面(Interface)與抽象類別(Abstract Class)的概念說明,相信讀者對於這兩者的差異有了初步的瞭解,但對於物件導向的初學者來說,或許還是充滿著疑惑,所以我將使用範例及簡單的實作來加深印象。
在此之前我為各位整理一下在.NET中的介面與抽象類別兩者在實作時的相同及相異之處:


相同:


兩者都不能被具體化。
都必須去實作已宣告的『抽象』方法。
相異:


一個類別能實作很多介面,但只能繼承一個抽象類別。
介面所定義的每個方法都必須在延伸類別中實作,抽象類別則可以定義部分的方法是已經具有此方法的內容描述,而部分是定義必須在延伸類別中去實作的。這麼說或許讀者會與我早先說明的介面與抽象類別『都必須去實作已宣告抽象的方法』感到疑惑,這部分會在稍後做說明。
介面強調的是定義一些功能給較不相干的類別使用,抽象類別主要是應用在關係密切(is a)的類別中使用。
抽象類別一定屬於繼承架構,而且一定是父類別,介面可以讓毫無關係的類別來實作同一個介面,這點同時也呼應了第三點的說明。
說到這裡我們先從程式語法上來看介面與抽象類別。


介面(Interface)


interface InterFace1
{
   string Method1();
   int Method2();
   bool Method3();
}


抽象類別(Abstract Class)
public abstract class AbstractClass
{
   public abstract string Method1();
   public abstract int Method2();
   public bool Method3()
  {
      bool a = false;
      return false;
   }
}


各位應該可以看出其實兩者是很類似的,因為他們都包含了必須被客戶端類別程式實作的『抽象成員函式』,不過在介面中,每個成員函式都是需要被他的客戶端類別程式所實作的,而抽象類別中則可包含了一個不一定要客戶端類別程式實作的成員函數(非抽象成員函式),如這裡所看到的 Method3(),他已經有實作了該成員函式所需要的敘述內容也就是說可以預設給給他行為,這就是兩者不同點之一。不過若你把抽象類別的 Method3() 敘述內容拿掉,並在前面加上 abstract 的話,你會發現其實他跟介面是很類似的,若歸納來說其實介面可算是抽象類別的特例(special-case)。


由架構設計的觀念來說,使用抽象類別一定與繼承有關係,但一個客戶端類別程式只能存在一個繼承關係,然而一個客戶端的類別程式卻可以實作多個不同的介面,因此經常有人使用介面來解決多重繼承的問題。


再從另一個架構設計的角度來看,因為介面不能定義一些成員函式預設的行為,因此,若你恰好要設計給每一個實作的客戶端類別程式都有預設『共同』的行為時,此時若使用介面的話,你就必須一一的為實作過此介面的客戶端類別程式來新增同樣的程式內容,這是一件多麼費工的事情阿!而且不小心還會 Copy & Paste 錯了,相同功能的程式碼也會重複存放而不易維護呢!


若使用抽象類別時,你便可以應用他的特性,只要在客戶端類別程式的父類別(抽象類別),新增一個成員函式的預設行為便可。所以,在此之前你就必需考慮到所欲達到的功能是否有上述新增共用成員函式的必要性的必要性,才能審慎選擇到底是要用介面或者是抽象類別。這也說明了我所提到過的,在設計介面時要考慮到將來實作此介面的客戶端類別程式與介面本身之間是否具有高度的相關性,白話一點就是說你很難去解釋實作介面的客戶端類別程式是某個介面,這樣子的一個關係,如實作 .Net Framework 內建的 ICollecton 的客戶端類別程式時,你不能說他就是屬於 ICollection 這樣的本質關係,你只能說他『像是』一個ICollection,但是相反的若你可以解釋客戶端類別程式是某個類別時,就盡量修改成抽象類別來實作此功能,因為他們具有密切的相關性,在未來你想要新增共同的預設行為時,便不會干擾到使用了相同的抽象類別的『不同本質』之客戶端類別程式,而且可以避免上述的問題發生。


或許你還是存在很多的懷疑,所以接下來我拿網路上常看的的例子來為大家做更深入的陳述。現在我們考慮一個關於 Door 的概念,這個 Door 具有兩個行為方法分別是 Open() 與 Close(),我分別用介面與抽象類別來實作,


介面
interface InterFace1
{
   object Open();
   object Close();
}


抽象類別
public abstract class Door
{
   public abstract object Open();
   public abstract object Close();
}




以上介面與抽象類別其實是具備類似的功能。但現在問題來了,房子的主人要求你要增加警報的功能,若你是設計師我想你會很直覺的加上Alert()成員函式如下:


介面interface InterFace1
{
   object Open();
   object Close();
   object Alert();
}


抽象類別
public abstract class Door
{
   public abstract object Open();
   public abstract object Close();
   public abstract object Alert();
}


客戶端類別程式如下:


介面
public class AlarmDoor : InterFace1
{
   object Open(){}
   object Close(){}
   object Alert(){}



抽象類別public class AlarmDoor : Door
{
   public override object Open(){}
   public override object Close(){}
   public override object Alert(){}
}


但其實這種作法違反了物件導向的介面隔離原則(ISP),因為 Clients should not be forced to depend upon interfaces that they do not use,也就是說 Door 既有的功能Open() 及 Close() 其實是不會有太多改變的,且這樣的行為方法與 Alert() 在概念上來說是不應該混在一起的,因為若有些客戶端的類別程式是衍生或實作至 Door 時,若此時修改了Alert()的行為方法時,將影響到所有客戶端類別程式,因此盡量避免這樣的設計,以免除掉相互依賴性。


不過上面的敘述反映出兩個問題:


我們可能沒有理解清楚問題領域,AlarmDoor 在概念本質上到底是 Door 還是報警器?
如果我們對於問題領域的理解沒有問題,如:我們對於問題領域的分析發現AlarmDoor 在概念的本質上和 Door 是一致的,那麼我們在實作時就不能正確的揭示我們的設計意圖,因為在這兩個概念的定義上反映不出上述含義。
其實就 AlarmDoor 而言在本質上他是一個 Door 所以他可以是一個抽象類別,但 AlarmDoor 又有警報功能,就好像他可以完成一件警報工作,所以可以利用介面的方式來定義他。


public abstract class Door
{
   public abstract object Open();
   public abstract object Close();
}


public interface Alarm
{
   object Alert();
}


public class AlarmDoor1 : Door, Alarm
{
   public override object Open(){}
   public override object Close(){}
   public object Alert(){}
}


結論:


其實介面與抽象類別在設計時有個原則就是,介面隱含著 (like a) 的關係,而抽象類別存在著 (is a) 的關係,因此若你對於你要解決的問題本身有比較深入的瞭解後,不難分析出這兩者應用時的觀點,因此,只要多去思考問題本身的意涵後相信你應該可以迎刃而解,不再有疑惑了。

沒有留言:

張貼留言