如果說繼承是用來表明物件「屬於什麼」;那麼介面就是用來表明物件「能做什麼」。

如果說封裝是將物件視作一個整體,是隱藏複雜度;那麼介面就是封裝精神的體現。

如果說多型是指藉著繼承後能實作不同的行為的可能性達到擴展的彈性;那麼介面就是在實作多型。

介面就是這麼厲害,這麼瀟灑。介面就是我大哥,今天誰不服介面,對不起!我們不認識。

介面就像是針對類別的實作、物件的行為去做規定的一個契約書,會先定義好要實作這個介面的類別所必須要有的方法,而當我們建立符合這個介面的類別時,就必須實作出所有介面中定義好方法才可以。……這樣說起來實在太繞口,總而言之介面的核心概念只有一條:

我不在乎你是誰,我只在乎你能做什麼。

還是公司誠徵工程師的例子

由於介面基本上就是封裝繼承多型抽象之大雜燴,所以我們把前面多型的小明小華例子稍微修改來用吧。也就是以公司徵人的方式去理解介面。

介面就像是老闆開出來的要求列表,例如說:要會寫 C#、要會寫 SQL、要會 VB…等等,於是老闆就貼出了徵人啟示,要求新來的員工必須要有 IProgrammer 寫的能力:

public interface IProgrammer
{
    void WriteCSharp();
    void WriteSQL();
    void WriteVB();
}

特別注意和前面多型的例子的不同處,介面只需要先定義好該做的事,裡面怎麼做不需要管;所以只需要宣告要求的方法,不需要撰寫方法本體

於是今天小華就又(?)來面試了,但是他其實並不會寫 C#:

public class Hua : IProgrammer
{
    // Error: Hua 未實作 IProgrammer.WriteCSharp()

    public void WriteSQL() { /* Work */ }
    public void WriteVB() { /* Work */ }
}

這時候編譯器就會跳出錯誤了:很抱歉,你不符合我們 IProgrammer 的規定,因為我們只喜歡訓練精英(略),請你實作完之後再來。否則你就不能掛上我們 : IProgrammer 的頭銜。

雖然小華面試失敗了,不過至少小華幫我們示範了一件事:類別要標上介面的方法就和繼承一樣,在類別名稱後面加上 :

而小華走了之後,馬上就輪到小明開開心心地來應徵了,他不只會寫 C#、SQL 和 VB,甚至還會泡英式奶茶:

public class Ming : IProgrammer
{
    public void WriteCSharp() { /* Work */ }
    public void WriteSQL() { /* Work */ }
    public void WriteVB() { /* Work */ }
    public Tea MakeTea() { return new Tea(teaName: "MilkTea"); }
}

火速通過面試之後,老闆就讓小明上工了:

public void Work()
{
    IProgrammer programmer = new Ming();
    programmer.WriteCSharp();
    programmer.WriteCSharp();
    programmer.WriteCSharp();
}

在這個時候,小明已經不再只是小明,對老闆而言他就是一個符合應徵要求的工程師,這裡只需要「我(老闆)要求能做這些事的人」而不是「小明」,是 IProgrammer 而不是 Ming。在呼叫 IProgrammer programmer = new Ming() 的同時,這裡就只剩下一個無情的寫程式機器,再也沒有小明。

上面這句雖然和多型範例的說明九成一樣,但絕對不是我偷懶(真的),差異的地方就是介面的本質。可以稍加對照看看。

當然,小明仍然不准在上班時間泡英式奶茶。

public void Work()
{
    IProgrammer programmer = new Ming();
    programmer.WriteCSharp();
    programmer.MakeTea(); // Error: IProgrammer 未包含 MakeTea 的定義
}

多型的時候,這個原因是由於今天的小明是工程師,工程師不需要會泡奶茶。也就是說子類別替代父類別時不需要那些父類別不會的動作。此處也是一樣的精神,我們要的是一個會寫程式的工程師,不是介面規範上的東西我們不需要

這是達成關注點分離和職責分離必經的道路,習慣之後對於公開方法和私有方法也會有更進一步的心得,以公開方法實現介面的要求,配合私有方法拆解內部的複雜度,甚至組合多個介面讓類別具有多項能力,這個過程只能用舒爽來形容(前提是介面不要開得太爛的話啦)。

變更的彈性

介面作為特性的體現,更多的是概念上的東西。最後這一小段就讓我們聊聊這些部分。

在新訓時改變我想法最多的就是介面,在這之前我只會一個函式硬寫到底,而習慣從介面開始設計物件後,才開始從功能的角度去想要怎麼寫。

介面不同於前面各項特性是告訴我們物件應該有什麼特徵,而是要求我們用「不同功能的物件之間對接時,我們該怎麼處理」的角度去看待問題。

介面的核心概念在於提供了更多的彈性,更精確地說是變更的彈性

原本是連線到 MySQL 取得資料,哪天突然就必須更改成要連線到 MongoDB 取得資料;

原本是只要實作出使用者儲存訂單的操作,突然接到指令說使用者必須區分成一般使用者和尊爵用戶並且實作出不同的操作流程等等。

變更總是來得又急又快,而這也讓我們靜下來想,當我們把關注點放在整個邏輯的時候--

我需要的是「連線到 MySQL 取得資料」的工具嗎?並不是,只要是「能連線到資料庫、能取得資料」就好了。

我需要的是「以OOO技術替使用者建立訂單並儲存」的工具嗎?並不是,「替使用者建立訂單並儲存」才是最重要的。

資料庫是可以替換的,儲存訂單到資料庫的工具也是可以替換的,甚至替使用者建立訂單的過程也是可以替換的。

因此,從介面開始設計時最重要的是釐清「我需要的是什麼」,用介面定義一份契約,把使用對象和實作銜接起來;並且把實作隔離成可替換的部件,達到解除耦合的目標。

最後還是回到了關注點分離的議題:主流程、商業邏輯專心做好自己的事,他們只需要知道這個物件能夠提供拿到資料的方法就好。而實際上怎麼拿到資料,則由實作的物件內部去處理,也就是封裝的核心精神。

要理解介面的概念,訣竅在於把目光更集中在「功能」的角度。我們在理解物件的時候,可以知道冰箱是一個物件、冰櫃是一個物件、保冷袋是一個物件;但當我們在海邊釣到魚,想要找個地方保存的時候,我們需要的是冰箱嗎?是冰櫃嗎?是保冷袋嗎?

都不是,我們需要的是「能低溫保存食物的東西」而已,今天你能用冰箱從海邊運到你家也沒關係,只要你實作得出來,並且魚是新鮮的就好。於是我們把觀看物件的角度集中在它的功能上,我們針對我們的需求去定義好我們需要的功能,這就是介面。

我們定義好什麼叫做飛行,於是鴿子跟烏鴉都算是實作了飛行;我們定義好什麼叫做游泳,於是海豚跟鯨魚都算是實作了游泳。

只要你符合我需要的功能,達到我要的目的,不論你是誰,你如何實作,我都無所謂。

如此我們既達到了關注點分離,也保留了定好規則,將來可以使用不同實作的彈性;甚至將來接手的是另一個人,他看你的介面就能知道如何替換,替換時對象至少要能做到哪些事,今天他接到需求是上頭覺得保冷袋太 Low 了,我們要改用冰櫃車,他也有個接口/介面去指示他修改的方向。

而在兩個系統,或是兩個分層之間要介接的時候,只需要提供我這個功能需要的接口/介面給對方,就能讓對方知道他必須實作哪些功能,如果我們要把運魚的需求託付給貨運公司,他看介面就知道我們要的是「低溫保存食物」,便可以提供對應的服務/實做給我們。如此豈不美哉!

備註:一些有寫過前後端銜接的,或是做過一些小工具的到這邊可能會覺得有點熟悉。例如說:程式和使用者銜接的點,叫做使用者介面;前端跟後端交換資料的 API,叫做應用程式介面。所謂的介面/接口就是這麼一回事,這裡也不例外。

小結

到這邊我們就介紹完介面的部分了,希望各位朋友能夠大概感覺到介面的精神。

當然有些讀者看到這裡可能也會有點疑惑:

像上面的例子中 IProgrammer programmer = new Ming(); 當我們宣告的當下不就還是知道了我們實作的對象是 Ming 了嗎?這樣並沒有完全分離呀?(依賴注入熱身中)

等等諸如此類一堆問題,都也是我有過的想法,在接下來的新訓系列也將會逐漸說明,欲知後續如何,且待下回分曉!

本系列下一篇:菜雞與物件導向 (8): 內聚、耦合

參考資料

推薦系列文

同系列文章