如何使用Interface?
物件導向語言的3大特徵 : 繼承、封裝、多型
。早期的物件導向強調繼承
,而近代的物件導向則強調多型
。
多型
簡單的說就是對interface寫程式
(Code to an Interface),所以interface是現代PHP實踐物件導向最重要的功能。
除此之外,interface也是PHP實踐TDD的重要基石。
Version
PHP 5.0
定義
Interface是兩個物件的合約
,定義了合作物件該具備哪些功能
,而非綁死特定物件
。 也就是說,我們不在乎該物件是如何實踐功能,只要該物件能實踐
(implement) 該interface所定義的功能,我們就可以安心的使用該物件。因為可以同時有很多物件實踐同一個interface,所以稱為多型
(polymorphism)。
生活上的例子
Modern PHP書中提到了一個例子,作者要去邁阿密參加PHP Developer Conference,想要租車,租車公司提供了3個選擇 :
Hyundai Compact
Subaru Wagon
Bugatti Veyron
作者希望馬力大一點 (Hyndai Compact淘汰),因為沒有帶孩子,所以沒有空間需求 (Subaru Wagon淘汰),最後選擇租Bugatti Veyron。
重點不是在於作者最後租哪一輛車,而是在於為什麼作者有三台車子可以選擇,因為這三台車都有相同的interface,每一台車都有方向盤、油門踏板、剎車踏板、方向燈….等,只要我們會操作這些interface,就可以駕駛所有實踐這些interface的車子,所以這三台車子我們都可以選擇,這就是「多型」
。Modern PHP作者挑這個例子挑的很好,相信男生對車子比較有感。
再舉一個例子,事實上硬體很早就在使用interface概念,所以我們有USB、HDMI…等介面,我們挑隨身碟不用被特定廠牌型號給綁死,只要有USB介面即可。挑螢幕也不用被特定廠牌型號綁死,只要挑有HDMI介面即可。這就是多型
,但軟體卻一直到物件導向觀念才有interface,而傳統C語言並沒有interface概念。
實務上的例子
可依各種方式萃取資料的class
Modern PHP作者寫了一個DocumentStore
class,他可以從各種方式萃取其資料,如 :
- 從URL萃取HTML資料
- 從stream的方式萃取資料
- 從command line的方式輸入資料
且隨著科技的進步,相信將來會有新的通訊協定出現,而導致新的方式萃取資料。
1 | class DocumentStore |
第5行1
public function addDocument(IDocumentable $document)
addDocument()
函式允許我們加入各種$document
物件,但唯一的要求是 : 該物件必須實踐IDocumentable interface
。
1 | interface IDocumentable |
為了與class有所區別,interface命名字首會加上I以資區別。
PHP規定interface內的函式都要宣告成public
。
IDocumentable
interface告訴我們,所有實踐該interface的class都必須有getId()
與getContent()
兩個函式,我們並不關心這兩個函式是怎麼寫怎麼實作的,只要有實踐就行。
從URL萃取HTML資料
1 | class HtmlDocument implements IDocumentable |
第1行1
class HtmlDocument implements IDocumentable
implements
關鍵字表示HtmlDocument
class實踐了IDocumentable
interface。既然實踐了該interface,PHP就會幫我們檢查HtmlDocument class是否有實踐getId()
與getContect()
兩個函式。
10行1
2
3
4public function getId()
{
return $this->url;
}
實踐了IDocumentable
interface要求的getId()
函式。
15行1
2
3
4public function getContent()
{
...
}
實踐了IDocumentable
interface要求的getContent()
函式。
至於怎麼實踐不是我們的重點,這是演算法該去傷腦筋的,目前我們只關心物件導向部分。
從Stream的方式萃取資料
1 | class StreamDocument implements IDocumentable |
第1行1
class StreamDocument implements IDocumentable
StreamDocument
class實踐了IDocumentable
interface。既然實踐了該interface,PHP就會幫我們檢查StreamDocument
class是否有實踐getId()
與getContect()
兩個函式。
第12行1
2
3
4public function getId()
{
return 'resource-' . (int)$this->resource;
}
實踐了IDocumentable
interface要求的getId()
函式。
第17行1
2
3
4public function getContent()
{
...
}
實踐了IDocumentable
interface要求的getContent()
函式。
至於怎麼實踐不是我們的重點,這是演算法該去傷腦筋的,目前我們只關心物件導向部分。
從Command Line的方式輸入資料
1 | class CommandOutputDocument implements IDocumentable |
第1行1
class CommandOutputDocument implements IDocumentable
CommandOutputDocument
class實踐了IDocumentable
interface。既然實踐了該interface,PHP就會幫我們檢查CommandOutputDocument
class是否有實踐getId()
與getContect()
兩個函式。
10行1
2
3
4public function getId()
{
return $this->command;
}
實踐了IDocumentable
interface要求的getId()
函式。
第15行1
2
3
4public function getContent()
{
return shell_exec($this->command);
}
實踐了IDocumentable
interface要求的getContent()
函式。
至於怎麼實踐不是我們的重點,這是演算法該去傷腦筋的,目前我們只關心物件導向部分。
使用DocumentStore class
1 | $documentStore = new DocumentStore(); |
第3行1
2
3// Add HTML document
$htmlDoc = new HtmlDocument('https://php.net');
$documentStore->addDocument($htmlDoc);
建立一個$htmlDoc
物件,由於$htmlDoc
物件實踐了IDocumentable
interface,所以可以透過addDocument()
函式新增至documentStore
物件內部。
第7行1
2
3// Add stream document
$streamDoc = new StreamDocument(fopen('stream.txt', 'rb'));
$documentStore->addDocument($streamDoc);
建立一個$streamDoc
物件,由於$streamDoc
物件實踐了IDocumentable
interface,所以可以透過addDocument()
函式新增至documentStore
物件內部。
第11行1
2
3// Add terminal command document
$cmdDoc = new CommandOutputDocument('cat /etc/hosts');
$documentStore->addDocument($cmdDoc);
建立一個$cmdDoc
物件,由於$cmdDoc
物件實踐了IDocumentable
interface,所以可以透過addDocument()
函式新增至documentStore
物件內部。
至於未來呢?未來若有新的通訊協定而導致新的方式萃取資料,只要再寫一個class實踐IDocumentable
interface即可,一樣可以透過addDocument()
函式新增至documentStore
物件內部。
Summary
如範例所示,若當初addDocument的定義如下 :1
2
3
4public function addDocument(HtmlDocument $document)
{
...
}
這會導致addDocument()
只有$htmlDoc
物件能被addDocument()
函式使用,也就是綁死了HtmlDocument
class,使的DocumentStore
class與HtmlDocuement
class的耦合度很高,彈性很小,其他物件都不能用了。
但若改成 :1
2
3
4public function addDocument(IDocumentable $document)
{
...
}
我們看到了IDocumentable
interface讓addDocument()
函式沒有綁定於特定class與物件,只是綁定特定interface,因此只要能實踐該interface的所有物件,都可以被addDocument()
函式所使用,這種允許各種物件的機制,就是多型
,這是在傳統程序式語言(Procedure Language)的C語言所看不到的。
除此之外,我們只關心你有沒有實踐IDocumentable
interface,只要你有實踐該interface,就有getId()
與getContent()
兩個函式,我就可以使用這兩個函式,而不用擔心所使用的物件的演算法是如何。
Good Practice
一個檔案只使用一個Interface
就語法上,PHP允許一個檔案內包含多個interface :1
2
3
4
5
6
7
8
9interface IFoo
{
public function getFoo();
}
interface IBar
{
public function getBar();
}
但實務上,基於一個class一個檔案one class per file
原則,一個檔案也應該只包含一個interface。1 1事實上,其他程式語言(C++, C#, Java)也都是一個檔案只包含一個interface。
與其他程式語言比較
C++
1 | class IDocumentable { |
宣告interface :
- C++沒有提供
interface
關鍵字,要用class
模擬。 - 規定函式必須使用
pure virtual function
宣告。
實踐interface :
- 沒提供
implements
關鍵字,使用public 繼承
來實踐interface。
C#
1 | interface IDocumentable { |
宣告interface :
- 使用
interface
關鍵字。 - 函式不用宣告為
public
。
實踐interface :
- 沒提供
implements
關鍵字,使用繼承
來實踐interface。
Java
1 | public interface IDocumentable { |
宣告interface :
- 使用
interface
關鍵字。 - interface必須宣告成
public
。 - 函式不用宣告為
public
。
實踐interface :
- 使用
implements
關鍵字。 - class與函式都必須宣告成
public
。
Ruby
1 | class IDucumentable |
宣告interface :
- Ruby沒有提供
interface
關鍵字,要用class
模擬。
實踐interface :
- 沒提供
implements
關鍵字,使用繼承
來實踐interface。
PHP
1 | interface IDocumentable |
宣告interface :
- 使用
interface
關鍵字。 - interface不必宣告為
public
。 - 函式必須宣告為
public
。
實踐interface :
- 使用
implements
關鍵字。 - 函式必須宣告為
public
。
Conclusion
- 本文僅討論interface最初級的用法,要深入學習interface,就要去學習TDD/BDD、Refactor、SOLID與Design Pattern。
- 若對
物件導向
概念還是很模糊,最少要記住Code to an Interface
,只要能對interface寫程式,最少是個及格的物件導向程式。 多型
風格的物件導向優點是靈活好維護,很適合寫架構很大,且需求常常變動的專案,缺點是內部較耗記憶體,需要維護一個vtable來實踐多型
,且切換函式的速度較慢,這也是為什麼重視速度與記憶體的C語言不願意使用物件導向的原因。firmware、driver與OS的確不適合物件導向,因為很底層,但若是寫AP、Web與App,由於很上層,所以速度與記憶體的問題就不再是重點,反而該關心的是架構,因為AP、Web與App的邏輯通常很複雜且常常需要修改,如何讓程式的架構更好,更容易維護,更容易擴展,更容易重複使用,這才是重點。MVC
就是個典型的例子,MVC
比傳統寫法更耗記憶體且速度更慢,但因為架構分的清楚,所以更好維護。