什么是接口?如何在PHP中使用接口?本篇文章帶大家聊聊使用接口編寫更優(yōu)雅的 PHP 代碼,希望對大家有所幫助!
在編程中,確保代碼可讀、可維護(hù)、可擴(kuò)展和易于測試是很重要的;而使用接口,恰恰是我們改進(jìn)代碼中所有這些因素的方法之一。
目標(biāo)讀者
本文的目標(biāo)讀者是對 OOP(面向?qū)ο缶幊蹋└拍钣谢玖私獠⒃?PHP 中使用繼承的開發(fā)人員。如果你知道如何在 PHP 代碼中使用繼承,那么你應(yīng)該可以很好地理解本文。
什么是接口?
簡而言之,接口只是對類應(yīng)該做什么的描述,它們可用于確保實(shí)現(xiàn)該接口的任何類都將包括在其內(nèi)部定義的每個公共方法。
接口可以:
- 用于定義類的公共方法;
- 用于定義類的常量。
接口不可以:
- 被實(shí)例化;
- 用于定義類的私有或受保護(hù)方法;
- 用于定義類的屬性。
接口是用來定義一個類應(yīng)該包括的公共方法的。記住,你只需要在接口里定義方法的簽名,而不需要包含方法的主體(就像通常在類中看到的方法一樣)。**這是因?yàn)榻涌趦H用于定義對象之間的通信,而不是像在類中那樣定義通信和行為。**為了說明這個問題,下面展示了一個定義了幾個公共方法的示例接口:
interface DownloadableReport { public function getName(): string; public function getHeaders(): array; public function getData(): array; }
根據(jù) php.net 文檔我們可以知道,接口有兩個主要用途:
-
允許開發(fā)者創(chuàng)建不同類別的對象,這些對象可以互換使用,因?yàn)樗鼈儗?shí)現(xiàn)了相同的一個或多個接口。常見的例子包含:多個數(shù)據(jù)庫訪問服務(wù)、多個支付網(wǎng)關(guān)、不同的緩存策略等。不同的實(shí)現(xiàn)之間可以互換,而不需要對使用它們的代碼進(jìn)行任何修改。
-
允許函數(shù)或方法接受符合接口的參數(shù)并對其進(jìn)行操作,而不關(guān)心該對象還可以做什么或它是如何實(shí)現(xiàn)的。這些接口通常被命名為
Iterable
、Cacheable
、Renderable
等,來說明這些接口的實(shí)際含義。
在 PHP 中使用接口
接口是 OOP(面向?qū)ο缶幊蹋┐a庫的重要部分。接口能讓我們降低代碼耦合并提高可擴(kuò)展性。舉個例子,讓我們看看下面這個類:
class BlogReport { public function getName(): string { return 'Blog report'; } }
如你所見,我們定義了一個類,類中有一個函數(shù),返回一個字符串。這樣一來,我們定義了該方法的行為,所以我們知道 getName()
是如何返回字符串的。不過,假設(shè)我們在另一個類調(diào)用這個方法;這個類不需要關(guān)心這個字符串如何構(gòu)建的,它只關(guān)心該方法是否返回內(nèi)容。舉例來說,讓我們看看如何在另一個類調(diào)用此方法:
class ReportDownloadService { public function downloadPDF(BlogReport $report) { $name = $report->getName(); // 下載文件…… } }
盡管上面的代碼正常運(yùn)行,但我們設(shè)想一下,現(xiàn)在想給 UsersReport
類中增加下載用戶報告的功能。顯然,我們不能使用 ReportDownloadService
中的現(xiàn)有方法,因?yàn)槲覀円呀?jīng)強(qiáng)制規(guī)定方法只能傳遞 BlogReport
類。因此,我們必須修改把原有的下載方法名稱改掉(避免重名),然后另外再添加一個類似的方法,如下所示:
class ReportDownloadService { public function downloadBlogReportPDF(BlogReport $report) { $name = $report->getName(); // 下載文件…… } public function downloadUsersReportPDF(UsersReport $report) { $name = $report->getName(); // 下載文件…… } }
假設(shè)上面的方法中的下載文件部分(注釋掉的部分)使用了相同的代碼,而且我們可以將這些相同的代碼單獨(dú)寫成一個方法,但我們?nèi)詴幸恍┲貜?fù)的代碼(譯者注:指的是每個方法中都會有 $name = $report->getName();
)以及有多個幾乎相同的類的入口。這可能會給將來擴(kuò)展代碼或測試帶來額外的工作量。
例如,假設(shè)我們創(chuàng)建了一個新的 AnalyticsReport
;我們現(xiàn)在需要向該類添加一個新的 downloadAnalyticsReportPDF()
方法。你可以清晰的看到這個文件將如何膨脹(譯者注:指每增加一個類型,就要增加一個下載方法)。這就是一個使用接口的完美場景!
讓我們從創(chuàng)建第一個接口開始:讓我們將其命名為 DownloadableReport
,定義如下:
interface DownloadableReport { public function getName(): string; public function getHeaders(): array; public function getData(): array; }
我們現(xiàn)在可以更新 BlogReport
和 UsersReport
來實(shí)現(xiàn) DownloadableReport
接口,如下例所示。但是請注意,作為演示用途,我故意把 UsersReport
中的代碼寫錯了:
class BlogReport implements DownloadableReport { public function getName(): string { return 'Blog report'; } public function getHeaders(): array { return ['The headers go here']; } public function getData(): array { return ['The data for the report is here.']; } }
class UsersReport implements DownloadableReport { public function getName() { return ['Users Report']; } public function getData(): string { return 'The data for the report is here.'; } }
但當(dāng)我們嘗試運(yùn)行代碼的時候,我們將會收到錯誤,原因如下:
-
缺少
getHeaders()
方法. -
getName()
方法不包括接口的方法簽名中定義的返回類型。 -
getData()
方法定義了一個返回類型,但它與接口的方法簽名中定義的類型不同。
因此,為了修復(fù) UsersReport
使其正確實(shí)現(xiàn) DownloadableReport
接口,我們可以將其修改為:
class UsersReport implements DownloadableReport { public function getName(): string { return 'Users Report'; } public function getHeaders(): array { return []; } public function getData(): array { return ['The data for the report is here.']; } }
現(xiàn)在兩個報告類都實(shí)現(xiàn)了相同的接口,我們可以這樣更新我們的 ReportDownloadService
:
class ReportDownloadService { public function downloadReportPDF(DownloadableReport $report) { $name = $report->getName(); // 下載文件…… } }
我們現(xiàn)在可以把 UsersReport
或 BlogReport
對象傳入 downloadReportPDF
方法中,而且不會出現(xiàn)任何錯誤。這是因?yàn)槲覀冎涝搶ο髮?shí)現(xiàn)了報告類的必要方法,并且將返回我們期望的數(shù)據(jù)類型。
通過向方法傳遞了一個接口,而不是一個具體的類,我們可以根據(jù)方法的實(shí)際作用(而不是方法的實(shí)現(xiàn)原理)來解耦 ReportDownloadService
類和這些報告類。
如果我們想創(chuàng)建一個新的 AnalyticsReport
,我們可以讓它實(shí)現(xiàn)相同的接口。這樣一來,我們不必添加任何新的方法,只需要將報告對象傳遞給同一個的 downloadReportPDF()
方法。如果你正在構(gòu)建你自己的包或框架,接口可能對你特別有用。你只需要告訴使用者要實(shí)現(xiàn)哪個接口,然后他們就可以創(chuàng)建自己的類。例如,在 Laravel 中,我們可以通過實(shí)現(xiàn) IlluminateContractsCacheStore
接口來創(chuàng)建自己的自定義緩存驅(qū)動類。
除了能改進(jìn)代碼之外,我喜歡使用接口的另一個原因是 —— 它們起到了“代碼即文檔”的作用。例如,如果我想弄清楚一個類能做什么,不能做什么,我傾向于先看接口,然后再看實(shí)現(xiàn)它的類。接口能夠告訴我們所有可被調(diào)用的方法,而不需要我們過多地關(guān)心這些方法的底層實(shí)現(xiàn)方式是怎樣的。
值得注意的是,Laravel
中的“契約(contract)”和“接口(interface)”這兩個詞語是可互換的。根據(jù) Laravel 文檔,“契約是一組由框架提供的核心服務(wù)的接口”。所以,記?。?strong>契約是一個接口,但接口不一定是契約。通常情況下,契約只是框架提供的一個接口。關(guān)于使用契約的