深入探討PHP 5.0的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

Hyundai Compact

Subaru Wagon

Subaru Wagon

Bugatti Veyron

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DocumentStore 
{

protected $data = [];

public function addDocument(IDocumentable $document)
{

$key = $document->getId();
$value = $document->getContent();
$this->data[$key] = $value;
}

public function getDocuments()
{

return $this->data;
}
}

第5行

1
public function addDocument(IDocumentable $document)

addDocument()函式允許我們加入各種$document物件,但唯一的要求是 : 該物件必須實踐IDocumentable interface

1
2
3
4
5
interface IDocumentable 
{

public function getId();
public function getContent();
}

為了與class有所區別,interface命名字首會加上I以資區別。

PHP規定interface內的函式都要宣告成public

IDocumentable interface告訴我們,所有實踐該interface的class都必須有getId()getContent()兩個函式,我們並不關心這兩個函式是怎麼寫怎麼實作的,只要有實踐就行。

從URL萃取HTML資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class HtmlDocument implements IDocumentable 
{

protected $url;

public function __construct($url)
{

$this->url = $url;
}

public function getId()
{

return $this->url;
}

public function getContent()
{

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
$html = curl_exec($ch);
curl_close($ch);

return $html;
}
}

第1行

1
class HtmlDocument implements IDocumentable

implements關鍵字表示HtmlDocument class實踐了IDocumentable interface。既然實踐了該interface,PHP就會幫我們檢查HtmlDocument class是否有實踐getId()getContect()兩個函式。

在PHP實踐interface用的是implements,而繼承class用的是extends。

10行

1
2
3
4
public function getId() 
{

return $this->url;
}

實踐了IDocumentable interface要求的getId()函式。

15行

1
2
3
4
public function getContent() 
{

...
}

實踐了IDocumentable interface要求的getContent()函式。

至於怎麼實踐不是我們的重點,這是演算法該去傷腦筋的,目前我們只關心物件導向部分。

從Stream的方式萃取資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class StreamDocument implements IDocumentable 
{

protected $resource;
protected $buffer;

public function __construct($resource, $buffer = 4096)
{

$this->resource = $resource;
$this->buffer = $buffer;
}

public function getId()
{

return 'resource-' . (int)$this->resource;
}

public function getContent()
{

$streamContent = '';
rewind($this->resource);

while (feof($this->resource) === false) {
$streamContent .= fread($this->resource, $this->buffer);
}

return $streamContent;
}
}

第1行

1
class StreamDocument implements IDocumentable

StreamDocument class實踐了IDocumentable interface。既然實踐了該interface,PHP就會幫我們檢查StreamDocument class是否有實踐getId()getContect()兩個函式。

第12行

1
2
3
4
public function getId() 
{

return 'resource-' . (int)$this->resource;
}

實踐了IDocumentable interface要求的getId()函式。

第17行

1
2
3
4
public function getContent() 
{

...
}

實踐了IDocumentable interface要求的getContent()函式。

至於怎麼實踐不是我們的重點,這是演算法該去傷腦筋的,目前我們只關心物件導向部分。

從Command Line的方式輸入資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CommandOutputDocument implements IDocumentable 
{

protected $command;

public function __construct($command)
{

$this->command = $command;
}

public function getId()
{

return $this->command;
}

public function getContent()
{

return shell_exec($this->command);
}
}

第1行

1
class CommandOutputDocument implements IDocumentable

CommandOutputDocument class實踐了IDocumentable interface。既然實踐了該interface,PHP就會幫我們檢查CommandOutputDocument class是否有實踐getId()getContect()兩個函式。

10行

1
2
3
4
public function getId() 
{

return $this->command;
}

實踐了IDocumentable interface要求的getId()函式。

第15行

1
2
3
4
public function getContent() 
{

return shell_exec($this->command);
}

實踐了IDocumentable interface要求的getContent()函式。

至於怎麼實踐不是我們的重點,這是演算法該去傷腦筋的,目前我們只關心物件導向部分。

使用DocumentStore class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$documentStore = new DocumentStore();
// Add HTML document
$htmlDoc = new HtmlDocument('https://php.net');
$documentStore->addDocument($htmlDoc);

// Add stream document
$streamDoc = new StreamDocument(fopen('stream.txt', 'rb'));
$documentStore->addDocument($streamDoc);

// Add terminal command document
$cmdDoc = new CommandOutputDocument('cat /etc/hosts');
$documentStore->addDocument($cmdDoc);

print_r($documentStore->getDocuments());

第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
4
public function addDocument(HtmlDocument $document) 
{

...
}

這會導致addDocument()只有$htmlDoc物件能被addDocument()函式使用,也就是綁死了HtmlDocument class,使的DocumentStore class與HtmlDocuement class的耦合度很高,彈性很小,其他物件都不能用了。

但若改成 :

1
2
3
4
public 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
9
interface IFoo 
{

public function getFoo();
}

interface IBar
{

public function getBar();
}

但實務上,基於一個class一個檔案one class per file原則,一個檔案也應該只包含一個interface。1 1事實上,其他程式語言(C++, C#, Java)也都是一個檔案只包含一個interface。

與其他程式語言比較

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class IDocumentable {
public:
virtual int getId() = 0;
virtual string getContent() = 0;
};

class HtmlDocument : public IDocumentable {
public:
int getId() {
...
}
string getContent() {
...
}
};

宣告interface :

  • C++沒有提供interface關鍵字,要用class模擬。
  • 規定函式必須使用pure virtual function宣告。

實踐interface :

  • 沒提供implements關鍵字,使用public 繼承來實踐interface。

C#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface IDocumentable {
int getId();
string getContent();
};

class HtmlDocument : IDocumentable {
public int getId() {
...
}

public string getContent() {
...
}
}

宣告interface :

  • 使用interface關鍵字。
  • 函式不用宣告為public

實踐interface :

  • 沒提供implements關鍵字,使用繼承來實踐interface。

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface IDocumentable {
int getId();
string getContent();
};

public class HtmlDocument implements IDocumentable {
public int getId() {
...
}

public string getContent() {
...
}
}

宣告interface :

  • 使用interface關鍵字。
  • interface必須宣告成public
  • 函式不用宣告為public

實踐interface :

  • 使用implements關鍵字。
  • class與函式都必須宣告成public

Ruby

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class IDucumentable
def getId()
end
def getContent()
end
end

class HtmlDocument < IDocumentable
def getId()
...
end
def getId()
...
end
end

宣告interface :

  • Ruby沒有提供interface關鍵字,要用class模擬。

實踐interface :

  • 沒提供implements關鍵字,使用繼承來實踐interface。

PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface IDocumentable 
{

public function getID();
public function getContent();
}

class HtmlDocument implements IDocumentable
{

public function getID()
{

...
}

public function getContent()
{

...
}
}

宣告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比傳統寫法更耗記憶體且速度更慢,但因為架構分的清楚,所以更好維護。
2015-08-31