PHP開發:從程序化到面向對象
譯文教程詳情
• 難度: 中級
• 預計完成時間: 60分鐘
這份教程的誕生源自一年多之前Robert C.Martin在演講中帶給我的啟發。當時他的演講主題在于討論創造“***編程語言”的可能性。在過程中,他提出了這樣幾個問題:為什么會存在“***編程語言”?這樣的語言應具備哪些特性?但隨著他的講解,我從中發現另一種有趣的思路:每種編程范式都在無形中給程序員帶來諸多無法避免的局限性。為了正本溯源,我打算在正式進入PHP由程序化向面向對象轉變這一話題之前,先與大家分享一些理論知識。
范式局限
每種編程范式都限制了我們將想象轉化為現實的能力。這些范式去掉了一部分可行方案,卻納入另一些方案作為替代,但這一切都是為了實現同樣的表示效果。模塊化編程令程序規模受到制約,強迫程序員只能在對應模塊范疇之內施展拳腳,且每個模塊結尾都要以“go-to”來指向其它模塊。這種設定直接影響了程序成品的規模。另外,結構化編程與程序化編程方式去掉了“go-to”聲明,從而限制了程序員對序列、選擇以及迭代語句的調整能力。序列屬于變量賦值,選擇屬于if-else判斷,而迭代則屬于do-while循環。這些已經成為當下編程語言與范式的構建基石。
面向對象編程方式去掉了函數指針,同時引入多態特性。PHP使用指針的方式與C語言有所不同,但我們仍能從變量函數庫中找到這些函數指針的變體形式。這使得程序員能夠將某個變量的值當成函數名稱,從而實現以下內容:
- function foo() {
- echo "This is foo";
- }
- function bar($param) {
- echo "This is bar saying: $param";
- }
- $function = 'foo';
- $function(); // Goes into foo()
- $function = 'bar';
- $function('test'); // Goes into bar()
初看起來,這種特性似乎無關緊要。但仔細想想,大家一定會發現其中蘊含著極為強大的潛力。我們可以將一條變量作為參數發往某函數,然后讓該函數根據參數數值調用其它函數。這絕對非同小可。它使我們能夠在不了解函數功能的前提下對其進行調用,而且函數自身根本不會體現出任何差異。
這項技術也正是我們實現多態性調用的關鍵所在。
現在,我們姑且不談函數指針的作用,先來看看其工作機制。函數指針中其實已經隱藏著“go-to”聲明,或者至少以間接方式實現了與“go-to”相近的執行效果。這可不是什么好消息。事實上,PHP通過一種非常巧妙的方式在不直接使用的前提下實現“go-to”聲明。如前例所示,我需要首先在PHP中做出聲明。雖然這看起來不難理解,但在大型項目以及函數種類繁多且彼此關聯的情況下,我們還是很難準確做出判斷。而在C語言這邊,這種關系就變得更加晦澀且極難理解。
然而僅僅消除函數指針還遠遠不夠。面向對象的編程機制必然帶來替代方案,事實也確實如此,它包含著多態特性與一套簡單語法。重點來了,多態性正是面向對象編程的核心價值,即:控制流與源代碼在依賴關系上正好相反。
在上面的圖片中,我們描繪了一個簡單的例子:多態性如何在兩個不同范式之間發揮作用。在程序化或者結構化編程領域,控制流與源代碼在依賴關系上非常相似——二者都指向更具體的輸出行為。
而在面向對象編程方面,我們可以逆轉源代碼的依賴關系,使其指向抽象執行結果,并保持控制流仍舊指向具體執行結果。這一點至關重要,因為我們希望控制機制能盡可能觸及具體層面與代碼中的不穩定部分,這樣我們才能真正讓執行結果與預期相符。但在源代碼這邊,我們的要求卻恰好相反。對于源代碼,我們希望將具體結果與不穩定因素排除在外,從而簡化修改流程、讓改動盡量不影響其它代碼。這樣不穩定部分可以經常修正,但抽象部分則仍然有效。大家可以點擊此處閱讀由Robert C.Martin所撰寫的依賴倒置原則研究論文。
試手任務
在本章中,我們將創建一款簡單應用,旨在列出谷歌日程表及其事件提醒內容。首先,我們嘗試利用程序化方式進行開發,只涉及簡單功能、避免以任何形式使用類或對象。開發工作結束之后,我們更進一步、在不改動程序化代碼的前提下通過行為進行代碼整理。***,嘗試將其轉化為面向對象版本。
#p#
谷歌PHP API客戶端
谷歌專門針對PHP提供一套API客戶端,我們將利用它與自己的谷歌賬戶進行對接,從而對日程表服務加以操作。要想讓代碼正確起效,大家需要通過設定讓自己的谷歌賬戶接受來自日程表的查詢。
雖然這是本篇指南文章的重要前提,但并不能算主要內容。為了避免在這方面浪費太多篇幅,請大家直接參考官方說明文檔。各位不必擔心,整個設置過程非常簡單,而且只要五分鐘左右即可搞定。
本教程附帶的示例代碼中包含谷歌PHP API客戶端代碼,建議大家就使用這一套以確保整個學習過程與文章說明保持一致。另外,如果大家想嘗試自行安裝,請點擊此處查看官方說明文檔。
接下來按照指示向apiAccess.php文件中填寫信息。該文件在程序化與面向對象兩套實例中都會用到,因此大家不必在新版本中重復填寫。我在文件中留下了自己填寫的內容,這樣大家就能更輕松地找到對應位置并將其按自己的資料進行修改。
如果大家碰巧用的是NetBeans,我把各個項目文件保存在了包含有不同范例的文件夾當中。這樣大家可以輕松打開該項目,并點選Run——>Run Project在本地PHP服務器(要求使用PHP 5.4)上直接加以運行。
與谷歌API對接的客戶端庫為面向對象型。為了示例的正常運行,我編寫了一套小小的函數合集,其中囊括了本教程所需要的所有函數。通過這種方式,我們可以利用程序化層在面向對象客戶端庫之上進行軟件編寫,且代碼不會涉及任何對象。
如果大家打算快速測試自己的代碼與指向谷歌API的連接是否正常起效,則可以直接使用位于index.php文件中的代碼。它會列出賬戶中所有日程表信息,且應該至少有一套具備summary字段的日程表中包含您的姓名。如果日程表中存在聯系人生日信息,那么谷歌API將無法與之正常協作。不過大家不用驚慌,另選一套即可。
- require_once './google-api-php-client/src/Google_Client.php';
- require_once './google-api-php-client/src/contrib/Google_CalendarService.php';
- require_once __DIR__ . '/../apiAccess.php';
- require_once './functins_google_api.php';
- require_once './functions.php';
- session_start();
- $client = createClient();
- if(!authenticate($client)) return;
- listAllCalendars($client);
這個index.php文件將成為我們應用程序的入口點。我們不會使用任何Web框架或者其它復雜的機制。我們要做的只是簡單輸出一些HTML代碼而已。
程序化開發方案
現在我們已經了解了所需創建的目標以及所能使用的資源,接下來就是下載附件中的源代碼。我會提供代碼中的有用片段,但為了進一步了解全局情況,大家可能希望訪問其初始來源。
在這套方案中,我們只求成果能按預期生效。我們的代碼可能會顯得有些粗糙,而且其中只涉及以下幾個文件:
• index.php – 這是惟一一個我們需要通過瀏覽器直接訪問并轉向其GET參數的文件。
• functions_google_api.php – 囊括所有前面提到的谷歌API。
• functions.php – 一切奇跡在此發生。
functions.php將容納應用程序的所有執行過程。包括路由邏輯、表現以及一切值與行為全部發生于此。這款應用非常簡單,其主邏輯如下圖所示:
這里有一項名為doUserAction()的函數,它的生效與否取決于一條很長的if-else聲明;其它方法則根據GET變量中的參數決定調用情況。這些方法隨后利用API與谷歌日程表對接,并在屏幕上顯示出我們需要的任何結果。
- function printCalendarContents($client) {
- putTitle('These are you events for ' . getCalendar($client, $_GET['showThisCalendar'])['summary'] . ' calendar:');
- foreach (retrieveEvents($client, $_GET['showThisCalendar']) as $event) {
- print('<div style="font-size:10px;color:grey;">' . date('Y-m-d H:m', strtotime($event['created'])));
- putLink('?showThisEvent=' . htmlentities($event['id']) .
- '&calendarId=' . htmlentities($_GET['showThisCalendar']), $event['summary']);
- print('</div>');
- print('<br>');
- }
- }
這個例子恐怕要算我們此次編寫的代碼中最為復雜的函數。它所調用的是名為putTitle()的輔助函數,其作用是將某些經過格式調整的HTML輸出以充當標題。標題中將包含我們日程表的實際名稱,這是通過調用來自functions_google_api.php文件中的getCalendar()函數來實現的。返回的日歷信息是一個數組,其中包含一個summary字段,而這正是我們要找的內容。
$client變量被傳遞到我們的所有函數當中。它需要與谷歌API相連,不過這方面內容我們稍后再談。
接下來,我們整理一下日程表中的全部現有事件。這份數組列表由封裝在retrieveEvents()函數中的API請求運行得來。對于每個事件,我們都會顯示出其創建日期及標題。
其余部分代碼與我們之前討論過的內容相近,甚至更容易理解。大家可以抱著輕松的心情隨便看看,然后抖擻精神進軍下一章。
組織程序化代碼
我們當前的代碼完全沒問題,但我想我們可以通過調整使其以更合適的方式組織起來。大家可能已經從附帶的源代碼中發現,該項目所有已經組織完成的代碼都被命名為“GoogleCalProceduralOrganized”。
使用全局客戶端變量
在代碼組織工作中,***件讓人心煩的事在于,我們把$client變量作為參數推廣到全局以及嵌套函數的深層當中。程序化編程方案對這類情況提供了一種巧妙的解決辦法,即全局變量。由于$client是由index.php所定義,而從全局觀點來看,我們需要改變的只是函數對該變量的具體使用方式。因此我們不必改變$client參數,而只需進行如下處理:
- function printCalendars() {
- global $client;
- putTitle('These are your calendars:');
- foreach (getCalendarList($client)['items'] as $calendar) {
- putLink('?showThisCalendar=' . htmlentities($calendar['id']), $calendar['summary']);
- print('<br>');
- }
- }
大家不妨將現有代碼與附件中的代碼成品進行比較,看看二者有何不同之處。沒錯,我們并沒有將$client作為參數傳遞,而是在所有函數中使用global $client并將其作為只傳遞向谷歌API函數的參數。從技術角度看,即使是谷歌API函數也能夠使用來自全局的$client變量,但我認為***還是盡量保持API的獨立性。
#p#
從邏輯中分離表示
某些函數的作用非常明確——只用于在屏幕上輸出信息,但有些函數則用于判斷觸發條件,更有些函數身兼兩種作用。面對這種情況,我們往往***把這些存在特殊用途的函數放在屬于自己的文件當中。我們首先整理只用于屏幕信息輸出的函數,并將其轉移到functions_display.php文件當中。具體做法如下所示:
- function printHome() {
- print('Welcome to Google Calendar over NetTuts Example');
- }
- function printMenu() {
- putLink('?home', 'Home');
- putLink('?showCalendars', 'Show Calendars');
- putLink('?logout', 'Log Out');
- print('<br><br>');
- }
- function putLink($href, $text) {
- print(sprintf('<a href="%s" style="font-size:12px;margin-left:10px;">%s</a> | ', $href, $text));
- }
- function putTitle($text) {
- print(sprintf('<h3 style="font-size:16px;color:green;">%s</h3>', $text));
- }
- function putBlock($text) {
- print('<div display="block">'.$text.'</div>');
- }
要完成剩余的表示分離工作,我們需要從方法中提取出表示部分。下面我們就以單一方法為例演示這一過程:
- function printEventDetails() {
- global $client;
- foreach (retrieveEvents($_GET['calendarId']) as $event)
- if ($event['id'] == $_GET['showThisEvent']) {
- putTitle('Details for event: '. $event['summary']);
- putBlock('This event has status ' . $event['status']);
- putBlock('It was created at ' .
- date('Y-m-d H:m', strtotime($event['created'])) .
- ' and last updated at ' .
- date('Y-m-d H:m', strtotime($event['updated'])) . '.');
- putBlock('For this event you have to <strong>' . $event['summary'] . '</strong>.');
- }
- }
我們可以明顯看到,無論if聲明中的內容如何、其代碼都屬于表示代碼,而余下的部分則屬于業務邏輯。與其利用一個龐大的函數處理所有事務,我們更傾向于將其拆分為多個不同函數:
- function printEventDetails() {
- global $client;
- foreach (retrieveEvents($_GET['calendarId']) as $event)
- if (isCurrentEvent($event))
- putEvent($event);
- }
- function isCurrentEvent($event) {
- return $event['id'] == $_GET['showThisEvent'];
- }
分離工作完成后,業務邏輯就變得簡單易懂了。我們甚至提取了一個小型方法來檢測該事件是否就是當前事件。所有表示代碼現在都由名為putEvent($event)函數負責,且被保存在functions_display.php文件當中:
- function putEvent($event) {
- putTitle('Details for event: ' . $event['summary']);
- putBlock('This event has status ' . $event['status']);
- putBlock('It was created at ' .
- date('Y-m-d H:m', strtotime($event['created'])) .
- ' and last updated at ' .
- date('Y-m-d H:m', strtotime($event['updated'])) . '.');
- putBlock('For this event you have to <strong>' . $event['summary'] . '</strong>.');
- }
盡管該方法只負責顯示信息,但其功能仍需在對$event結構非常了解的前提下方能實現。不過對于我們的簡單實例來說,這已經足夠了。對于其余方法,大家可以通過類似的方式進行分離。
清除過長的if-else聲明
目前代碼整理工作還剩下***一步,也就是存在于doUserAction()函數中的過長if-else聲明,其作用是決定每項行為的實際處理方式。在元編程方面(通過引用來調用函數),PHP具備相當出色的靈活性。這種特性使我們能夠將$_GET變量的值與函數名稱關聯起來。如此一來,我們可以在$_GET變量中引入單獨的action參數,并將該值作為函數名稱。
- function doUserAction() {
- putMenu();
- if (!isset($_GET['action'])) return;
- $_GET['action']();
- }
基于這種方式,我們生成的菜單將如下所示:
- function putMenu() {
- putLink('?action=putHome', 'Home');
- putLink('?action=printCalendars', 'Show Calendars');
- putLink('?logout', 'Log Out');
- print('<br><br>');
- }
如大家所見,經過重新整理之后,代碼已經呈現出面向對象式設計的特性。雖然目前我們還不清楚其面向的是何種對象、會執行哪些確切行為,但其特征已經初露端倪。
我們已經讓來自業務邏輯的數據類型成為表示的決定性因素,其效果與我們在文首介紹環節中談到的依賴倒置機制比較類似。控制流的方向仍然是從業務邏輯指向表示,但源代碼依賴性則與之相反。從這一點上看,我認為整套機制更像是一種雙向依賴體系。
設計傾向上的面向對象化還體現在另一個方面,即我們幾乎沒有涉及到元編程。我們可以調用一個方法,但卻對其一無所知。該方法可以擁有任何內容,且過程與處理低級多態性非常相近。
依賴性分析
對于當前代碼我們可以繪制出一份關系圖,內容如下所示。通過這幅關系圖,我們可以看到應用程序運行流程的前幾個步驟。當然,把整套流程都畫下來就太過復雜了。
藍色線條代表程序調用。如大家所見,這些線條與始終指向同一個方向。圖中的綠色線條則表示間接調用,可以看到所有間接調用都要經過doUserAction()函數。這兩種線條代表控制流,顯然控制流的走向基本不變。
紅色線條則引入了完全不同的概念,它們代表著最初的源代碼依賴關系。之所以說“最初”,是因為隨著應用的運行其指向將變得愈發復雜、難以把握。putMenu()方法中包含著被特定關系所調用的函數的名稱。這是一種依賴關系,同時也是適用于所有其它關系創建方法的基本規則。它們的具體關系取決于其它函數的行為。
上圖中我們還能看到另一種依賴關系,即對數據的依賴。我前面曾經提到過$calendar與$event,輸出函數需要清楚了解這些數組的內部結構才能實現既定功能。
完成了以上內容之后,我們已經做好充分準備、可以迎來本篇教程中的***一項挑戰。
#p#
面向對象解決方案
無論采用哪種范式,我們都不可能為問題找到***的解決方案。因此以下代碼組織方式僅僅屬于我的個人建議。
從直覺出發
我們已經完成了業務邏輯與表現的分離工作,甚至將doUserAction()方法作為一個獨立單元。那么我的直覺是先創建三個類,Presenter、Logic與Router。三者以后可能都需要進行調整,但我們不妨先從這里著手,對吧?
Router中將只包含一個方法,且實現方式與之前提到的方法非常相似。
- class Router {
- function doUserAction() {
- (new Presenter())->putMenu();
- if (!isset($_GET['action']))
- return;
- (new Logic())->$_GET['action']();
- }
- }
現在我們要做的是利用剛剛創建的Presenter對象調用putMenu()方法,其它行為則利用Logic對象加以調用。不過這樣會馬上產生問題——我們的一項行為并不包含在Logic類當中。putHome()存在于Presenter類中,我們需要在Logic中引入一項行為,借以在Presenter中作為putHome()方法的委托。請記住,目前我們要做的只是將現有代碼整理到三個類當中,并將三者作為面向對象設計的備選對象。現在所做的一切只是為了讓設計方案能夠正常運作,待代碼編寫完成后、我們將進一步加以調試。
在將putHome()方法引入Logic類后,我們又遇上新的難題。怎樣才能從Presenter中調用方法?我們可以創建一個Presenter對象,并將其傳遞至Logic當中。下面我們從Router類入手。
- class Router {
- function doUserAction() {
- (new Presenter())->putMenu();
- if (!isset($_GET['action']))
- return;
- (new Logic(new Presenter))->$_GET['action']();
- }
- }
現在我們可以向Logic添加一個構造函數,并將其添加到Presenter內指向putHome()的委托當中。
- class Logic {
- private $presenter;
- function __construct(Presenter $presenter) {
- $this->presenter = $presenter;
- }
- function putHome() {
- $this->presenter->putHome();
- }
- [...]
- }
通過對index.php的一些小小調整、讓Presenter包含原有display方法、Logic包含原有業務邏輯函數、Router包含原有行為選擇符,我們已經可以讓自己的代碼正常運行并具備“Home”菜單元素。
- require_once './google-api-php-client/src/Google_Client.php';
- require_once './google-api-php-client/src/contrib/Google_CalendarService.php';
- require_once __DIR__ . '/../apiAccess.php';
- require_once './functins_google_api.php';
- require_once './Presenter.php';
- require_once './Logic.php';
- require_once './Router.php';
- session_start();
- $client = createClient();
- if(!authenticate($client)) return;
- (new Router())->doUserAction();
下面就是其執行效果。
接下來,我們需要在Logic類中適當變更指向display邏輯的調用指令,從而與$this->presenter相符。現在我們有兩個方法——isCurrentEvent()與retrieveEvents()——二者只被用于Logic類內部。我們將其作為專用方法,并據此變更調用關系。
下面我們對Presenter類進行同樣處理,并將所有指向方法的調用都變更為指向$this->something。由于putTitle()、putLink()與putBlock()都只由Presenter使用,因此需要將其變為專用。如果感到上述變更過程難于理解及操作,請大家查看附件源代碼內GoogleCalObjectOrientedInitial文件夾中的已完成代碼。
現在我們的應用程序已經能夠正常運行,這些按面向對象語法整理過的程序化代碼仍然使用$client全局變量,且擁有大量其它非面向對象式特性——但仍然能夠正常運行。
如果要為目前的代碼繪制依賴關系類圖,則應如下所示:
控制流與源代碼的依賴關系都通過Router、然后是Logic、***通過表示層。***一步變更削弱了我們在之前步驟中所觀察到的依賴倒置特性,但大家千萬不要因此受到迷惑——原理依然如故,我們要做的是使其更加清晰。
恢復源代碼依賴關系
很難界定基礎性原則之間哪一條更重要,但我認為依賴倒置原則對我們的應用設計影響***也最直接。該原則規定:
A:高層模塊不應依賴于低級模塊,二者都應依賴于抽象。
B:抽象不應依賴于細節,細節應依賴于抽象。
簡單來說,這意味著具體實施應依賴于抽象類。類越趨近抽象,它們就越不容易發生改變。因此我們可以這樣理解:變更頻繁的類應依賴于其它更為穩定的類。所以任何應用中最不穩定的部分很可能是用戶界面,這在我們的應用示例中通過Presenter類來實現。讓我們再來明確一下依賴倒置流程。
首先,我們讓Router僅使用Presenter,并打破其對Logic的依賴關系。
- class Router {
- function doUserAction() {
- (new Presenter())->putMenu();
- if (!isset($_GET['action']))
- return;
- (new Presenter())->$_GET['action']();
- }
- }
然后我們變更Presenter,使其使用Logic實例并由此獲取需要的信息。在我們的例子中,我認為由Presenter來建立該Logic實例也可以接受,但在生產系統當中、大家可能通常會利用Factories來創建與對象相關的業務邏輯,并將其注入表示層當中。
現在,原本同時存在于Logic與Presenter兩個類中的putHome()函數將從Logic中消失。這一現象說明我們已經開始進行重復數據清除工作。指向Presenter的構造函數與引用也從Logic中消失了。另一方面,由構造函數所創建的Logic對象則必須被寫入Presenter。
- class Presenter {
- private $businessLogic;
- function __construct() {
- $this->businessLogic = new Logic();
- }
- function putHome() {
- print('Welcome to Google Calendar over NetTuts Example');
- }
- [...]
- }
以上變更完成之后,點擊Show Calendars,屏幕上會出現錯誤提示。由于我們鏈接內部的所有行為都指向Logic類中的函數名稱,因此必須通過更多一致性調整來恢復二者之間的依賴關系。下面我們對方法進行一一修改,先來看***條錯誤信息:
- Fatal error: Call to undefined method Presenter::printCalendars()
- in /[...]/GoogleCalObjectOrientedFinal/Router.php on line 9
我們的Router希望調用Presenter中某個并不存在的方法,也就是printCalendars()。我們在Presenter中創建這樣一個方法,并檢查它會對Logic造成哪些影響。在結果中大家可以看到,它輸出了一條標題,并在重復循環之后再次調用putCalendars()。在Presenter類中,printCalendars()方法如下所示:
- function printCalendars() {
- $this->putCalendarListTitle();
- foreach ($this->businessLogic->getCalendars() as $calendar) {
- $this->putCalendarListElement($calendar);
- }
- }
在Logic方面,該方法則非常單純——直接調用谷歌API庫。
- function getCalendars() {
- global $client;
- return getCalendarList($client)['items'];
- }
這可能讓大家心中出現兩個問題,“我們真的需要Logic類嗎?”以及“我們的應用程序是否存在任何邏輯?”好吧,目前我們還不知道答案,現在能做的只是繼續上述過程,直到所有代碼都能正常工作且Logic不再依賴于Presenter。
接下來,我們將使用Presenter中的printCalendarContents()方法,如下所示:
- function printCalendarContents() {
- $this->putCalendarTitle();
- foreach ($this->businessLogic->getEventsForCalendar() as $event) {
- $this->putEventListElement($event);
- }
- }
這將反過來允許我們簡化Logic中的getEventsForCalendar(),并將其轉化為如下形式:
- function getEventsForCalendar() {
- global $client;
- return getEventList($client, htmlspecialchars($_GET['showThisCalendar']))['items'];
- }
現在應用已經不再報錯,但我卻又發現了新的問題。$_GET變量同時被Logic與Presenter類所使用——$_GET應該只被Presenter類使用才對。我的意思是,由于需要創建用于填充$_GET變量的鏈接,Presenter是肯定需要感知$_GET的。這就意味著$_GET與HTTP密切相關。現在,我們希望自己的代碼能與命令行或者桌面圖形用戶界面協同運作。
#p#
因此我們需要保證只有Presenter感知到這一情況,即將以上兩個方法變換為下列內容:
- function getEventsForCalendar($calendarId) {
- global $client;
- return getEventList($client, $calendarId)['items'];
- }
- function printCalendarContents() {
- $this->putCalendarTitle();
- $eventsForCalendar = $this->businessLogic->getEventsForCalendar(htmlspecialchars($_GET['showThisCalendar']));
- foreach ($eventsForCalendar as $event) {
- $this->putEventListElement($event);
- }
- }
現在我們需要實現特定事件的輸出功能。對于本文中的范例,我們假設自己無法直接檢索任何事件,即必須親自進行事件查找。Logic類這時候就要派上用場了,我們可以在其中操作事件列表并搜索特定ID:
- function getEventById($eventId, $calendarId) {
- foreach ($this->getEventsForCalendar($calendarId) as $event)
- if ($event['id'] == $eventId)
- return $event;
- }
然后Presenter的對應調用會完成輸出工作:
- function printEventDetails() {
- $this->putEvent(
- $this->businessLogic->getEventById(
- $_GET['showThisEvent'],
- $_GET['calendarId']
- )
- );
- }
就是這樣,我們已經成功完成了依賴倒置。
控制流仍然由Logic指向Presenter,所有輸出內容也完全由Logic進行定義。這樣如果我們打算接入其它日程表服務,則只需創建另一個Logic類并將其注入Presenter即可,Presenter本身不會感知到任何差異。再有,源代碼依賴關系也被成功倒置。Presenter是惟一創建且直接依賴于Logic的類。這種依賴關系對于保證Presenter可隨意變更數據顯示方式而又不影響Logic內容而言至關重要。此外,這種依賴關系允許我們利用CLI Presenter或者其它任何向用戶顯示信息的方法來替代HTML Presenter。
擺脫全局變量
現在惟一漏網的潛在設計缺陷就只剩下$client全局變量了。應用程序中的所有代碼都會對其進行訪問,但與之形成鮮明對比的是,真正有必要訪問$client的只有Logic類一個。最直觀的解決辦法肯定是使其變更為專用類變量,但這樣一來我們就需要將$client經由Router傳遞至Presenter處,從而使presenter能夠利用$client變更創建出Logic對象——這對于解決問題顯然無甚作用。我們的設計初衷是在獨立環境下建立類,并準確為其分配依賴關系。
對于任何大型類結構,我們都傾向于使用Factories;但在本文的小小范例中,index.php文件已經足以容納邏輯創建了。作為應用程序的入口點,這個類似于高層體系結構中“main”的文件仍然處于業務邏輯的范疇之外。
因此我們將index.php中的代碼變更為以下內容,同時保留所有內容以及session_start()指令:
- $client = createClient();
- if(!authenticate($client)) return;
- $logic = new Logic($client);
- $presenter = new Presenter($logic);
- (new Router($presenter))->doUserAction();
結語
現在工作徹底完成了。當然,我們的設計肯定還有很多改進的空間。我們可以為Logic類中的方法編寫一些測試流程,也許Logic類本身也可以換個更有代表性的名稱,例如GoogleCalendarGateway。我們還可以創建Event與Calendar類,從而更好地控制相關數據及行為,同時將Presenter的依賴關系根據數據類型拆分為數組。另一項改進與擴展方針則是創建多態性行為類,用于取代直接通過$_GET調用函數。總而言之,對于這一范例的改進可謂無窮無盡,有興趣的朋友可以嘗試將自己的想法轉化為現實。我在附件的GoogleCalObjectOrientedFinal文件夾中保存有代碼的最終版本,大家能夠以此為起點進行探索。
如果大家的好奇心比較強,也可以試著將這款小應用與其它日程表服務對接,看看如何在不同平臺上以不同方式實現信息輸出。對于使用NetBeans的朋友,每個源代碼文件夾中都包含有NetBeans項目,大家只要直接打開即可。在最終版本中,PHPUnit也已經準備就緒。不過我在其它項目中將其移除了——因為還沒有經過測試。
感謝您的閱讀。
附件下載地址:https://github.com/tutsplus/From-Procedural-to-Object-Oriented-PHP
原文鏈接:http://net.tutsplus.com/tutorials/php/from-procedural-to-object-oriented-php/