自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

PHP開發(fā):從程序化到面向?qū)ο?/h1> 譯文

開發(fā) 后端 前端
這份教程的誕生源自一年多之前Robert C.Martin在演講中帶給我的啟發(fā)。當(dāng)時(shí)他的演講主題在于討論創(chuàng)造“終極編程語言”的可能性。在過程中,他提出了這樣幾個(gè)問題:為什么會(huì)存在“終極編程語言”?這樣的語言應(yīng)具備哪些特性?但隨著他的講解,我從中發(fā)現(xiàn)另一種有趣的思路:每種編程范式都在無形中給程序員帶來諸多無法避免的局限性。

教程詳情

• 難度: 中級(jí)

• 預(yù)計(jì)完成時(shí)間: 60分鐘

這份教程的誕生源自一年多之前Robert C.Martin在演講中帶給我的啟發(fā)。當(dāng)時(shí)他的演講主題在于討論創(chuàng)造“***編程語言”的可能性。在過程中,他提出了這樣幾個(gè)問題:為什么會(huì)存在“***編程語言”?這樣的語言應(yīng)具備哪些特性?但隨著他的講解,我從中發(fā)現(xiàn)另一種有趣的思路:每種編程范式都在無形中給程序員帶來諸多無法避免的局限性。為了正本溯源,我打算在正式進(jìn)入PHP由程序化向面向?qū)ο筠D(zhuǎn)變這一話題之前,先與大家分享一些理論知識(shí)。

范式局限

每種編程范式都限制了我們將想象轉(zhuǎn)化為現(xiàn)實(shí)的能力。這些范式去掉了一部分可行方案,卻納入另一些方案作為替代,但這一切都是為了實(shí)現(xiàn)同樣的表示效果。模塊化編程令程序規(guī)模受到制約,強(qiáng)迫程序員只能在對(duì)應(yīng)模塊范疇之內(nèi)施展拳腳,且每個(gè)模塊結(jié)尾都要以“go-to”來指向其它模塊。這種設(shè)定直接影響了程序成品的規(guī)模。另外,結(jié)構(gòu)化編程與程序化編程方式去掉了“go-to”聲明,從而限制了程序員對(duì)序列、選擇以及迭代語句的調(diào)整能力。序列屬于變量賦值,選擇屬于if-else判斷,而迭代則屬于do-while循環(huán)。這些已經(jīng)成為當(dāng)下編程語言與范式的構(gòu)建基石。

面向?qū)ο缶幊谭绞饺サ袅撕瘮?shù)指針,同時(shí)引入多態(tài)特性。PHP使用指針的方式與C語言有所不同,但我們?nèi)阅軓淖兞亢瘮?shù)庫中找到這些函數(shù)指針的變體形式。這使得程序員能夠?qū)⒛硞€(gè)變量的值當(dāng)成函數(shù)名稱,從而實(shí)現(xiàn)以下內(nèi)容:

  1. function foo() {  
  2.     echo "This is foo";  
  3. }  
  4.    
  5. function bar($param) {  
  6.     echo "This is bar saying: $param";  
  7. }  
  8.    
  9. $function = 'foo';  
  10. $function();        // Goes into foo()  
  11.    
  12. $function = 'bar';  
  13. $function('test');  // Goes into bar()  

初看起來,這種特性似乎無關(guān)緊要。但仔細(xì)想想,大家一定會(huì)發(fā)現(xiàn)其中蘊(yùn)含著極為強(qiáng)大的潛力。我們可以將一條變量作為參數(shù)發(fā)往某函數(shù),然后讓該函數(shù)根據(jù)參數(shù)數(shù)值調(diào)用其它函數(shù)。這絕對(duì)非同小可。它使我們能夠在不了解函數(shù)功能的前提下對(duì)其進(jìn)行調(diào)用,而且函數(shù)自身根本不會(huì)體現(xiàn)出任何差異。

這項(xiàng)技術(shù)也正是我們實(shí)現(xiàn)多態(tài)性調(diào)用的關(guān)鍵所在。

現(xiàn)在,我們姑且不談函數(shù)指針的作用,先來看看其工作機(jī)制。函數(shù)指針中其實(shí)已經(jīng)隱藏著“go-to”聲明,或者至少以間接方式實(shí)現(xiàn)了與“go-to”相近的執(zhí)行效果。這可不是什么好消息。事實(shí)上,PHP通過一種非常巧妙的方式在不直接使用的前提下實(shí)現(xiàn)“go-to”聲明。如前例所示,我需要首先在PHP中做出聲明。雖然這看起來不難理解,但在大型項(xiàng)目以及函數(shù)種類繁多且彼此關(guān)聯(lián)的情況下,我們還是很難準(zhǔn)確做出判斷。而在C語言這邊,這種關(guān)系就變得更加晦澀且極難理解。

然而僅僅消除函數(shù)指針還遠(yuǎn)遠(yuǎn)不夠。面向?qū)ο蟮木幊虣C(jī)制必然帶來替代方案,事實(shí)也確實(shí)如此,它包含著多態(tài)特性與一套簡(jiǎn)單語法。重點(diǎn)來了,多態(tài)性正是面向?qū)ο缶幊痰暮诵膬r(jià)值,即:控制流與源代碼在依賴關(guān)系上正好相反。

 

在上面的圖片中,我們描繪了一個(gè)簡(jiǎn)單的例子:多態(tài)性如何在兩個(gè)不同范式之間發(fā)揮作用。在程序化或者結(jié)構(gòu)化編程領(lǐng)域,控制流與源代碼在依賴關(guān)系上非常相似——二者都指向更具體的輸出行為。

而在面向?qū)ο缶幊谭矫?,我們可以逆轉(zhuǎn)源代碼的依賴關(guān)系,使其指向抽象執(zhí)行結(jié)果,并保持控制流仍舊指向具體執(zhí)行結(jié)果。這一點(diǎn)至關(guān)重要,因?yàn)槲覀兿M刂茩C(jī)制能盡可能觸及具體層面與代碼中的不穩(wěn)定部分,這樣我們才能真正讓執(zhí)行結(jié)果與預(yù)期相符。但在源代碼這邊,我們的要求卻恰好相反。對(duì)于源代碼,我們希望將具體結(jié)果與不穩(wěn)定因素排除在外,從而簡(jiǎn)化修改流程、讓改動(dòng)盡量不影響其它代碼。這樣不穩(wěn)定部分可以經(jīng)常修正,但抽象部分則仍然有效。大家可以點(diǎn)擊此處閱讀由Robert C.Martin所撰寫的依賴倒置原則研究論文。

試手任務(wù)

在本章中,我們將創(chuàng)建一款簡(jiǎn)單應(yīng)用,旨在列出谷歌日程表及其事件提醒內(nèi)容。首先,我們嘗試?yán)贸绦蚧绞竭M(jìn)行開發(fā),只涉及簡(jiǎn)單功能、避免以任何形式使用類或?qū)ο蟆i_發(fā)工作結(jié)束之后,我們更進(jìn)一步、在不改動(dòng)程序化代碼的前提下通過行為進(jìn)行代碼整理。***,嘗試將其轉(zhuǎn)化為面向?qū)ο蟀姹尽?/p>

#p#

谷歌PHP API客戶端

谷歌專門針對(duì)PHP提供一套API客戶端,我們將利用它與自己的谷歌賬戶進(jìn)行對(duì)接,從而對(duì)日程表服務(wù)加以操作。要想讓代碼正確起效,大家需要通過設(shè)定讓自己的谷歌賬戶接受來自日程表的查詢。

雖然這是本篇指南文章的重要前提,但并不能算主要內(nèi)容。為了避免在這方面浪費(fèi)太多篇幅,請(qǐng)大家直接參考官方說明文檔。各位不必?fù)?dān)心,整個(gè)設(shè)置過程非常簡(jiǎn)單,而且只要五分鐘左右即可搞定。

本教程附帶的示例代碼中包含谷歌PHP API客戶端代碼,建議大家就使用這一套以確保整個(gè)學(xué)習(xí)過程與文章說明保持一致。另外,如果大家想嘗試自行安裝,請(qǐng)點(diǎn)擊此處查看官方說明文檔。

接下來按照指示向apiAccess.php文件中填寫信息。該文件在程序化與面向?qū)ο髢商讓?shí)例中都會(huì)用到,因此大家不必在新版本中重復(fù)填寫。我在文件中留下了自己填寫的內(nèi)容,這樣大家就能更輕松地找到對(duì)應(yīng)位置并將其按自己的資料進(jìn)行修改。

如果大家碰巧用的是NetBeans,我把各個(gè)項(xiàng)目文件保存在了包含有不同范例的文件夾當(dāng)中。這樣大家可以輕松打開該項(xiàng)目,并點(diǎn)選Run——>Run Project在本地PHP服務(wù)器(要求使用PHP 5.4)上直接加以運(yùn)行。

與谷歌API對(duì)接的客戶端庫為面向?qū)ο笮?。為了示例的正常運(yùn)行,我編寫了一套小小的函數(shù)合集,其中囊括了本教程所需要的所有函數(shù)。通過這種方式,我們可以利用程序化層在面向?qū)ο罂蛻舳藥熘线M(jìn)行軟件編寫,且代碼不會(huì)涉及任何對(duì)象。

如果大家打算快速測(cè)試自己的代碼與指向谷歌API的連接是否正常起效,則可以直接使用位于index.php文件中的代碼。它會(huì)列出賬戶中所有日程表信息,且應(yīng)該至少有一套具備summary字段的日程表中包含您的姓名。如果日程表中存在聯(lián)系人生日信息,那么谷歌API將無法與之正常協(xié)作。不過大家不用驚慌,另選一套即可。

  1. require_once './google-api-php-client/src/Google_Client.php';  
  2. require_once './google-api-php-client/src/contrib/Google_CalendarService.php';  
  3. require_once __DIR__ . '/../apiAccess.php';  
  4. require_once './functins_google_api.php';  
  5. require_once './functions.php';  
  6. session_start();  
  7.    
  8. $client = createClient();  
  9. if(!authenticate($client)) return;  
  10. listAllCalendars($client);  

這個(gè)index.php文件將成為我們應(yīng)用程序的入口點(diǎn)。我們不會(huì)使用任何Web框架或者其它復(fù)雜的機(jī)制。我們要做的只是簡(jiǎn)單輸出一些HTML代碼而已。

程序化開發(fā)方案

現(xiàn)在我們已經(jīng)了解了所需創(chuàng)建的目標(biāo)以及所能使用的資源,接下來就是下載附件中的源代碼。我會(huì)提供代碼中的有用片段,但為了進(jìn)一步了解全局情況,大家可能希望訪問其初始來源。

在這套方案中,我們只求成果能按預(yù)期生效。我們的代碼可能會(huì)顯得有些粗糙,而且其中只涉及以下幾個(gè)文件:

index.php – 這是惟一一個(gè)我們需要通過瀏覽器直接訪問并轉(zhuǎn)向其GET參數(shù)的文件。

functions_google_api.php – 囊括所有前面提到的谷歌API。

functions.php – 一切奇跡在此發(fā)生。

functions.php將容納應(yīng)用程序的所有執(zhí)行過程。包括路由邏輯、表現(xiàn)以及一切值與行為全部發(fā)生于此。這款應(yīng)用非常簡(jiǎn)單,其主邏輯如下圖所示:

 

這里有一項(xiàng)名為doUserAction()的函數(shù),它的生效與否取決于一條很長(zhǎng)的if-else聲明;其它方法則根據(jù)GET變量中的參數(shù)決定調(diào)用情況。這些方法隨后利用API與谷歌日程表對(duì)接,并在屏幕上顯示出我們需要的任何結(jié)果。

  1. function printCalendarContents($client) {  
  2.    putTitle('These are you events for ' . getCalendar($client$_GET['showThisCalendar'])['summary'] . ' calendar:');  
  3.    foreach (retrieveEvents($client$_GET['showThisCalendar']) as $event) {  
  4.       print('<div style="font-size:10px;color:grey;">' . date('Y-m-d H:m'strtotime($event['created'])));  
  5.       putLink('?showThisEvent=' . htmlentities($event['id']) .  
  6.             '&calendarId=' . htmlentities($_GET['showThisCalendar']), $event['summary']);  
  7.       print('</div>');  
  8.       print('<br>');  
  9.    }  
  10. }  

這個(gè)例子恐怕要算我們此次編寫的代碼中最為復(fù)雜的函數(shù)。它所調(diào)用的是名為putTitle()的輔助函數(shù),其作用是將某些經(jīng)過格式調(diào)整的HTML輸出以充當(dāng)標(biāo)題。標(biāo)題中將包含我們?nèi)粘瘫淼膶?shí)際名稱,這是通過調(diào)用來自functions_google_api.php文件中的getCalendar()函數(shù)來實(shí)現(xiàn)的。返回的日歷信息是一個(gè)數(shù)組,其中包含一個(gè)summary字段,而這正是我們要找的內(nèi)容。

$client變量被傳遞到我們的所有函數(shù)當(dāng)中。它需要與谷歌API相連,不過這方面內(nèi)容我們稍后再談。

接下來,我們整理一下日程表中的全部現(xiàn)有事件。這份數(shù)組列表由封裝在retrieveEvents()函數(shù)中的API請(qǐng)求運(yùn)行得來。對(duì)于每個(gè)事件,我們都會(huì)顯示出其創(chuàng)建日期及標(biāo)題。

 

其余部分代碼與我們之前討論過的內(nèi)容相近,甚至更容易理解。大家可以抱著輕松的心情隨便看看,然后抖擻精神進(jìn)軍下一章。

組織程序化代碼

我們當(dāng)前的代碼完全沒問題,但我想我們可以通過調(diào)整使其以更合適的方式組織起來。大家可能已經(jīng)從附帶的源代碼中發(fā)現(xiàn),該項(xiàng)目所有已經(jīng)組織完成的代碼都被命名為“GoogleCalProceduralOrganized”。

使用全局客戶端變量

在代碼組織工作中,***件讓人心煩的事在于,我們把$client變量作為參數(shù)推廣到全局以及嵌套函數(shù)的深層當(dāng)中。程序化編程方案對(duì)這類情況提供了一種巧妙的解決辦法,即全局變量。由于$client是由index.php所定義,而從全局觀點(diǎn)來看,我們需要改變的只是函數(shù)對(duì)該變量的具體使用方式。因此我們不必改變$client參數(shù),而只需進(jìn)行如下處理:

  1. function printCalendars() {  
  2.    global $client;  
  3.    putTitle('These are your calendars:');  
  4.    foreach (getCalendarList($client)['items'as $calendar) {  
  5.       putLink('?showThisCalendar=' . htmlentities($calendar['id']), $calendar['summary']);  
  6.       print('<br>');  
  7.    }  
  8. }  

大家不妨將現(xiàn)有代碼與附件中的代碼成品進(jìn)行比較,看看二者有何不同之處。沒錯(cuò),我們并沒有將$client作為參數(shù)傳遞,而是在所有函數(shù)中使用global $client并將其作為只傳遞向谷歌API函數(shù)的參數(shù)。從技術(shù)角度看,即使是谷歌API函數(shù)也能夠使用來自全局的$client變量,但我認(rèn)為***還是盡量保持API的獨(dú)立性。

#p#

從邏輯中分離表示

某些函數(shù)的作用非常明確——只用于在屏幕上輸出信息,但有些函數(shù)則用于判斷觸發(fā)條件,更有些函數(shù)身兼兩種作用。面對(duì)這種情況,我們往往***把這些存在特殊用途的函數(shù)放在屬于自己的文件當(dāng)中。我們首先整理只用于屏幕信息輸出的函數(shù),并將其轉(zhuǎn)移到functions_display.php文件當(dāng)中。具體做法如下所示:

  1. function printHome() {  
  2.    print('Welcome to Google Calendar over NetTuts Example');  
  3. }  
  4.    
  5. function printMenu() {  
  6.    putLink('?home''Home');  
  7.    putLink('?showCalendars''Show Calendars');  
  8.    putLink('?logout''Log Out');  
  9.    print('<br><br>');  
  10. }  
  11.    
  12. function putLink($href$text) {  
  13.    print(sprintf('<a href="%s" style="font-size:12px;margin-left:10px;">%s</a> | '$href$text));  
  14. }  
  15.    
  16. function putTitle($text) {  
  17.    print(sprintf('<h3 style="font-size:16px;color:green;">%s</h3>'$text));  
  18. }  
  19.    
  20. function putBlock($text) {  
  21.    print('<div display="block">'.$text.'</div>');  
  22. }  

要完成剩余的表示分離工作,我們需要從方法中提取出表示部分。下面我們就以單一方法為例演示這一過程:

  1. function printEventDetails() {  
  2.    global $client;  
  3.    foreach (retrieveEvents($_GET['calendarId']) as $event)  
  4.       if ($event['id'] == $_GET['showThisEvent']) {  
  5.          putTitle('Details for event: '$event['summary']);  
  6.          putBlock('This event has status ' . $event['status']);  
  7.          putBlock('It was created at ' .  
  8.                date('Y-m-d H:m'strtotime($event['created'])) .  
  9.                ' and last updated at ' .  
  10.                date('Y-m-d H:m'strtotime($event['updated'])) . '.');  
  11.          putBlock('For this event you have to <strong>' . $event['summary'] . '</strong>.');  
  12.       }  
  13. }  

我們可以明顯看到,無論if聲明中的內(nèi)容如何、其代碼都屬于表示代碼,而余下的部分則屬于業(yè)務(wù)邏輯。與其利用一個(gè)龐大的函數(shù)處理所有事務(wù),我們更傾向于將其拆分為多個(gè)不同函數(shù):

  1. function printEventDetails() {  
  2.    global $client;  
  3.    foreach (retrieveEvents($_GET['calendarId']) as $event)  
  4.       if (isCurrentEvent($event))  
  5.          putEvent($event);  
  6. }  
  7.    
  8. function isCurrentEvent($event) {  
  9.    return $event['id'] == $_GET['showThisEvent'];  
  10. }  

分離工作完成后,業(yè)務(wù)邏輯就變得簡(jiǎn)單易懂了。我們甚至提取了一個(gè)小型方法來檢測(cè)該事件是否就是當(dāng)前事件。所有表示代碼現(xiàn)在都由名為putEvent($event)函數(shù)負(fù)責(zé),且被保存在functions_display.php文件當(dāng)中:

  1. function putEvent($event) {  
  2.    putTitle('Details for event: ' . $event['summary']);  
  3.    putBlock('This event has status ' . $event['status']);  
  4.    putBlock('It was created at ' .  
  5.          date('Y-m-d H:m'strtotime($event['created'])) .  
  6.          ' and last updated at ' .  
  7.          date('Y-m-d H:m'strtotime($event['updated'])) . '.');  
  8.    putBlock('For this event you have to <strong>' . $event['summary'] . '</strong>.');  
  9. }  

盡管該方法只負(fù)責(zé)顯示信息,但其功能仍需在對(duì)$event結(jié)構(gòu)非常了解的前提下方能實(shí)現(xiàn)。不過對(duì)于我們的簡(jiǎn)單實(shí)例來說,這已經(jīng)足夠了。對(duì)于其余方法,大家可以通過類似的方式進(jìn)行分離。

清除過長(zhǎng)的if-else聲明

目前代碼整理工作還剩下***一步,也就是存在于doUserAction()函數(shù)中的過長(zhǎng)if-else聲明,其作用是決定每項(xiàng)行為的實(shí)際處理方式。在元編程方面(通過引用來調(diào)用函數(shù)),PHP具備相當(dāng)出色的靈活性。這種特性使我們能夠?qū)?_GET變量的值與函數(shù)名稱關(guān)聯(lián)起來。如此一來,我們可以在$_GET變量中引入單獨(dú)的action參數(shù),并將該值作為函數(shù)名稱。

  1. function doUserAction() {  
  2.    putMenu();  
  3.    if (!isset($_GET['action'])) return;  
  4.       $_GET['action']();  
  5. }  

基于這種方式,我們生成的菜單將如下所示:

  1. function putMenu() {  
  2.    putLink('?action=putHome''Home');  
  3.    putLink('?action=printCalendars''Show Calendars');  
  4.    putLink('?logout''Log Out');  
  5.    print('<br><br>');  
  6. }  

如大家所見,經(jīng)過重新整理之后,代碼已經(jīng)呈現(xiàn)出面向?qū)ο笫皆O(shè)計(jì)的特性。雖然目前我們還不清楚其面向的是何種對(duì)象、會(huì)執(zhí)行哪些確切行為,但其特征已經(jīng)初露端倪。

我們已經(jīng)讓來自業(yè)務(wù)邏輯的數(shù)據(jù)類型成為表示的決定性因素,其效果與我們?cè)谖氖捉榻B環(huán)節(jié)中談到的依賴倒置機(jī)制比較類似??刂屏鞯姆较蛉匀皇菑臉I(yè)務(wù)邏輯指向表示,但源代碼依賴性則與之相反。從這一點(diǎn)上看,我認(rèn)為整套機(jī)制更像是一種雙向依賴體系。

設(shè)計(jì)傾向上的面向?qū)ο蠡€體現(xiàn)在另一個(gè)方面,即我們幾乎沒有涉及到元編程。我們可以調(diào)用一個(gè)方法,但卻對(duì)其一無所知。該方法可以擁有任何內(nèi)容,且過程與處理低級(jí)多態(tài)性非常相近。

依賴性分析

對(duì)于當(dāng)前代碼我們可以繪制出一份關(guān)系圖,內(nèi)容如下所示。通過這幅關(guān)系圖,我們可以看到應(yīng)用程序運(yùn)行流程的前幾個(gè)步驟。當(dāng)然,把整套流程都畫下來就太過復(fù)雜了。

 

藍(lán)色線條代表程序調(diào)用。如大家所見,這些線條與始終指向同一個(gè)方向。圖中的綠色線條則表示間接調(diào)用,可以看到所有間接調(diào)用都要經(jīng)過doUserAction()函數(shù)。這兩種線條代表控制流,顯然控制流的走向基本不變。

紅色線條則引入了完全不同的概念,它們代表著最初的源代碼依賴關(guān)系。之所以說“最初”,是因?yàn)殡S著應(yīng)用的運(yùn)行其指向?qū)⒆兊糜l(fā)復(fù)雜、難以把握。putMenu()方法中包含著被特定關(guān)系所調(diào)用的函數(shù)的名稱。這是一種依賴關(guān)系,同時(shí)也是適用于所有其它關(guān)系創(chuàng)建方法的基本規(guī)則。它們的具體關(guān)系取決于其它函數(shù)的行為。

上圖中我們還能看到另一種依賴關(guān)系,即對(duì)數(shù)據(jù)的依賴。我前面曾經(jīng)提到過$calendar與$event,輸出函數(shù)需要清楚了解這些數(shù)組的內(nèi)部結(jié)構(gòu)才能實(shí)現(xiàn)既定功能。

完成了以上內(nèi)容之后,我們已經(jīng)做好充分準(zhǔn)備、可以迎來本篇教程中的***一項(xiàng)挑戰(zhàn)。

#p#

面向?qū)ο蠼鉀Q方案

無論采用哪種范式,我們都不可能為問題找到***的解決方案。因此以下代碼組織方式僅僅屬于我的個(gè)人建議。

從直覺出發(fā)

我們已經(jīng)完成了業(yè)務(wù)邏輯與表現(xiàn)的分離工作,甚至將doUserAction()方法作為一個(gè)獨(dú)立單元。那么我的直覺是先創(chuàng)建三個(gè)類,Presenter、Logic與Router。三者以后可能都需要進(jìn)行調(diào)整,但我們不妨先從這里著手,對(duì)吧?

Router中將只包含一個(gè)方法,且實(shí)現(xiàn)方式與之前提到的方法非常相似。

  1. class Router {  
  2.    
  3.    function doUserAction() {  
  4.       (new Presenter())->putMenu();  
  5.       if (!isset($_GET['action']))  
  6.          return;  
  7.       (new Logic())->$_GET['action']();  
  8.    }  
  9.    
  10. }  

現(xiàn)在我們要做的是利用剛剛創(chuàng)建的Presenter對(duì)象調(diào)用putMenu()方法,其它行為則利用Logic對(duì)象加以調(diào)用。不過這樣會(huì)馬上產(chǎn)生問題——我們的一項(xiàng)行為并不包含在Logic類當(dāng)中。putHome()存在于Presenter類中,我們需要在Logic中引入一項(xiàng)行為,借以在Presenter中作為putHome()方法的委托。請(qǐng)記住,目前我們要做的只是將現(xiàn)有代碼整理到三個(gè)類當(dāng)中,并將三者作為面向?qū)ο笤O(shè)計(jì)的備選對(duì)象。現(xiàn)在所做的一切只是為了讓設(shè)計(jì)方案能夠正常運(yùn)作,待代碼編寫完成后、我們將進(jìn)一步加以調(diào)試。

在將putHome()方法引入Logic類后,我們又遇上新的難題。怎樣才能從Presenter中調(diào)用方法?我們可以創(chuàng)建一個(gè)Presenter對(duì)象,并將其傳遞至Logic當(dāng)中。下面我們從Router類入手。

  1. class Router {  
  2.    
  3.    function doUserAction() {  
  4.       (new Presenter())->putMenu();  
  5.       if (!isset($_GET['action']))  
  6.          return;  
  7.       (new Logic(new Presenter))->$_GET['action']();  
  8.    }  
  9.    
  10. }  

現(xiàn)在我們可以向Logic添加一個(gè)構(gòu)造函數(shù),并將其添加到Presenter內(nèi)指向putHome()的委托當(dāng)中。

  1. class Logic {  
  2.    
  3.    private $presenter;  
  4.    
  5.    function __construct(Presenter $presenter) {  
  6.       $this->presenter = $presenter;  
  7.    }  
  8.    
  9.    function putHome() {  
  10.       $this->presenter->putHome();  
  11.    }  
  12.    
  13. [...]  
  14.    
  15. }  

通過對(duì)index.php的一些小小調(diào)整、讓Presenter包含原有display方法、Logic包含原有業(yè)務(wù)邏輯函數(shù)、Router包含原有行為選擇符,我們已經(jīng)可以讓自己的代碼正常運(yùn)行并具備“Home”菜單元素。

  1. require_once './google-api-php-client/src/Google_Client.php';  
  2. require_once './google-api-php-client/src/contrib/Google_CalendarService.php';  
  3. require_once __DIR__ . '/../apiAccess.php';  
  4. require_once './functins_google_api.php';  
  5. require_once './Presenter.php';  
  6. require_once './Logic.php';  
  7. require_once './Router.php';  
  8. session_start();  
  9.    
  10. $client = createClient();  
  11. if(!authenticate($client)) return;  
  12.    
  13. (new Router())->doUserAction();  

下面就是其執(zhí)行效果。

 

接下來,我們需要在Logic類中適當(dāng)變更指向display邏輯的調(diào)用指令,從而與$this->presenter相符?,F(xiàn)在我們有兩個(gè)方法——isCurrentEvent()與retrieveEvents()——二者只被用于Logic類內(nèi)部。我們將其作為專用方法,并據(jù)此變更調(diào)用關(guān)系。 

下面我們對(duì)Presenter類進(jìn)行同樣處理,并將所有指向方法的調(diào)用都變更為指向$this->something。由于putTitle()、putLink()與putBlock()都只由Presenter使用,因此需要將其變?yōu)閷S谩H绻械缴鲜鲎兏^程難于理解及操作,請(qǐng)大家查看附件源代碼內(nèi)GoogleCalObjectOrientedInitial文件夾中的已完成代碼。

現(xiàn)在我們的應(yīng)用程序已經(jīng)能夠正常運(yùn)行,這些按面向?qū)ο笳Z法整理過的程序化代碼仍然使用$client全局變量,且擁有大量其它非面向?qū)ο笫教匦?mdash;—但仍然能夠正常運(yùn)行。

如果要為目前的代碼繪制依賴關(guān)系類圖,則應(yīng)如下所示:

控制流與源代碼的依賴關(guān)系都通過Router、然后是Logic、***通過表示層。***一步變更削弱了我們?cè)谥安襟E中所觀察到的依賴倒置特性,但大家千萬不要因此受到迷惑——原理依然如故,我們要做的是使其更加清晰。

恢復(fù)源代碼依賴關(guān)系

很難界定基礎(chǔ)性原則之間哪一條更重要,但我認(rèn)為依賴倒置原則對(duì)我們的應(yīng)用設(shè)計(jì)影響***也最直接。該原則規(guī)定:

A:高層模塊不應(yīng)依賴于低級(jí)模塊,二者都應(yīng)依賴于抽象。

B:抽象不應(yīng)依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)依賴于抽象。

簡(jiǎn)單來說,這意味著具體實(shí)施應(yīng)依賴于抽象類。類越趨近抽象,它們就越不容易發(fā)生改變。因此我們可以這樣理解:變更頻繁的類應(yīng)依賴于其它更為穩(wěn)定的類。所以任何應(yīng)用中最不穩(wěn)定的部分很可能是用戶界面,這在我們的應(yīng)用示例中通過Presenter類來實(shí)現(xiàn)。讓我們?cè)賮砻鞔_一下依賴倒置流程。

首先,我們讓Router僅使用Presenter,并打破其對(duì)Logic的依賴關(guān)系。

  1. class Router {  
  2.    
  3.    function doUserAction() {  
  4.       (new Presenter())->putMenu();  
  5.       if (!isset($_GET['action']))  
  6.          return;  
  7.       (new Presenter())->$_GET['action']();  
  8.    }  
  9.    
  10. }  

然后我們變更Presenter,使其使用Logic實(shí)例并由此獲取需要的信息。在我們的例子中,我認(rèn)為由Presenter來建立該Logic實(shí)例也可以接受,但在生產(chǎn)系統(tǒng)當(dāng)中、大家可能通常會(huì)利用Factories來創(chuàng)建與對(duì)象相關(guān)的業(yè)務(wù)邏輯,并將其注入表示層當(dāng)中。

現(xiàn)在,原本同時(shí)存在于Logic與Presenter兩個(gè)類中的putHome()函數(shù)將從Logic中消失。這一現(xiàn)象說明我們已經(jīng)開始進(jìn)行重復(fù)數(shù)據(jù)清除工作。指向Presenter的構(gòu)造函數(shù)與引用也從Logic中消失了。另一方面,由構(gòu)造函數(shù)所創(chuàng)建的Logic對(duì)象則必須被寫入Presenter。

  1. class Presenter {  
  2.    
  3.    private $businessLogic;  
  4.    
  5.    function __construct() {  
  6.       $this->businessLogic = new Logic();  
  7.    }  
  8.    
  9.    function putHome() {  
  10.       print('Welcome to Google Calendar over NetTuts Example');  
  11.    }  
  12.    
  13. [...]  
  14.    
  15. }  

以上變更完成之后,點(diǎn)擊Show Calendars,屏幕上會(huì)出現(xiàn)錯(cuò)誤提示。由于我們鏈接內(nèi)部的所有行為都指向Logic類中的函數(shù)名稱,因此必須通過更多一致性調(diào)整來恢復(fù)二者之間的依賴關(guān)系。下面我們對(duì)方法進(jìn)行一一修改,先來看***條錯(cuò)誤信息:

  1. Fatal error: Call to undefined method Presenter::printCalendars()  
  2. in /[...]/GoogleCalObjectOrientedFinal/Router.php on line 9 

我們的Router希望調(diào)用Presenter中某個(gè)并不存在的方法,也就是printCalendars()。我們?cè)赑resenter中創(chuàng)建這樣一個(gè)方法,并檢查它會(huì)對(duì)Logic造成哪些影響。在結(jié)果中大家可以看到,它輸出了一條標(biāo)題,并在重復(fù)循環(huán)之后再次調(diào)用putCalendars()。在Presenter類中,printCalendars()方法如下所示:

  1. function printCalendars() {  
  2.    $this->putCalendarListTitle();  
  3.    foreach ($this->businessLogic->getCalendars() as $calendar) {  
  4.       $this->putCalendarListElement($calendar);  
  5.    }  
  6. }  

在Logic方面,該方法則非常單純——直接調(diào)用谷歌API庫。

  1. function getCalendars() {  
  2.    global $client;  
  3.    return getCalendarList($client)['items'];  
  4. }  

這可能讓大家心中出現(xiàn)兩個(gè)問題,“我們真的需要Logic類嗎?”以及“我們的應(yīng)用程序是否存在任何邏輯?”好吧,目前我們還不知道答案,現(xiàn)在能做的只是繼續(xù)上述過程,直到所有代碼都能正常工作且Logic不再依賴于Presenter。

接下來,我們將使用Presenter中的printCalendarContents()方法,如下所示:

  1. function printCalendarContents() {  
  2.    $this->putCalendarTitle();  
  3.    foreach ($this->businessLogic->getEventsForCalendar() as $event) {  
  4.       $this->putEventListElement($event);  
  5.    }  
  6. }  

這將反過來允許我們簡(jiǎn)化Logic中的getEventsForCalendar(),并將其轉(zhuǎn)化為如下形式:

  1. function getEventsForCalendar() {  
  2.    global $client;  
  3.    return getEventList($client, htmlspecialchars($_GET['showThisCalendar']))['items'];  
  4. }  

現(xiàn)在應(yīng)用已經(jīng)不再報(bào)錯(cuò),但我卻又發(fā)現(xiàn)了新的問題。$_GET變量同時(shí)被Logic與Presenter類所使用——$_GET應(yīng)該只被Presenter類使用才對(duì)。我的意思是,由于需要?jiǎng)?chuàng)建用于填充$_GET變量的鏈接,Presenter是肯定需要感知$_GET的。這就意味著$_GET與HTTP密切相關(guān)?,F(xiàn)在,我們希望自己的代碼能與命令行或者桌面圖形用戶界面協(xié)同運(yùn)作。

#p#

因此我們需要保證只有Presenter感知到這一情況,即將以上兩個(gè)方法變換為下列內(nèi)容:

  1. function getEventsForCalendar($calendarId) {  
  2.    global $client;  
  3.    return getEventList($client$calendarId)['items'];  
  4. }  
  1. function printCalendarContents() {  
  2.    $this->putCalendarTitle();  
  3.    $eventsForCalendar = $this->businessLogic->getEventsForCalendar(htmlspecialchars($_GET['showThisCalendar']));  
  4.    foreach ($eventsForCalendar as $event) {  
  5.       $this->putEventListElement($event);  
  6.    }  
  7. }  

現(xiàn)在我們需要實(shí)現(xiàn)特定事件的輸出功能。對(duì)于本文中的范例,我們假設(shè)自己無法直接檢索任何事件,即必須親自進(jìn)行事件查找。Logic類這時(shí)候就要派上用場(chǎng)了,我們可以在其中操作事件列表并搜索特定ID:

  1. function getEventById($eventId$calendarId) {  
  2.    foreach ($this->getEventsForCalendar($calendarIdas $event)  
  3.       if ($event['id'] == $eventId)  
  4.          return $event;  
  5. }  

然后Presenter的對(duì)應(yīng)調(diào)用會(huì)完成輸出工作:

  1. function printEventDetails() {  
  2.    $this->putEvent(  
  3.       $this->businessLogic->getEventById(  
  4.          $_GET['showThisEvent'],  
  5.          $_GET['calendarId']  
  6.       )  
  7.    );  
  8. }  

就是這樣,我們已經(jīng)成功完成了依賴倒置。

 

控制流仍然由Logic指向Presenter,所有輸出內(nèi)容也完全由Logic進(jìn)行定義。這樣如果我們打算接入其它日程表服務(wù),則只需創(chuàng)建另一個(gè)Logic類并將其注入Presenter即可,Presenter本身不會(huì)感知到任何差異。再有,源代碼依賴關(guān)系也被成功倒置。Presenter是惟一創(chuàng)建且直接依賴于Logic的類。這種依賴關(guān)系對(duì)于保證Presenter可隨意變更數(shù)據(jù)顯示方式而又不影響Logic內(nèi)容而言至關(guān)重要。此外,這種依賴關(guān)系允許我們利用CLI Presenter或者其它任何向用戶顯示信息的方法來替代HTML Presenter。

擺脫全局變量

現(xiàn)在惟一漏網(wǎng)的潛在設(shè)計(jì)缺陷就只剩下$client全局變量了。應(yīng)用程序中的所有代碼都會(huì)對(duì)其進(jìn)行訪問,但與之形成鮮明對(duì)比的是,真正有必要訪問$client的只有Logic類一個(gè)。最直觀的解決辦法肯定是使其變更為專用類變量,但這樣一來我們就需要將$client經(jīng)由Router傳遞至Presenter處,從而使presenter能夠利用$client變更創(chuàng)建出Logic對(duì)象——這對(duì)于解決問題顯然無甚作用。我們的設(shè)計(jì)初衷是在獨(dú)立環(huán)境下建立類,并準(zhǔn)確為其分配依賴關(guān)系。

對(duì)于任何大型類結(jié)構(gòu),我們都傾向于使用Factories;但在本文的小小范例中,index.php文件已經(jīng)足以容納邏輯創(chuàng)建了。作為應(yīng)用程序的入口點(diǎn),這個(gè)類似于高層體系結(jié)構(gòu)中“main”的文件仍然處于業(yè)務(wù)邏輯的范疇之外。

因此我們將index.php中的代碼變更為以下內(nèi)容,同時(shí)保留所有內(nèi)容以及session_start()指令:

  1. $client = createClient();  
  2. if(!authenticate($client)) return;  
  3.    
  4. $logic = new Logic($client);  
  5. $presenter = new Presenter($logic);  
  6. (new Router($presenter))->doUserAction();  

結(jié)語

現(xiàn)在工作徹底完成了。當(dāng)然,我們的設(shè)計(jì)肯定還有很多改進(jìn)的空間。我們可以為L(zhǎng)ogic類中的方法編寫一些測(cè)試流程,也許Logic類本身也可以換個(gè)更有代表性的名稱,例如GoogleCalendarGateway。我們還可以創(chuàng)建Event與Calendar類,從而更好地控制相關(guān)數(shù)據(jù)及行為,同時(shí)將Presenter的依賴關(guān)系根據(jù)數(shù)據(jù)類型拆分為數(shù)組。另一項(xiàng)改進(jìn)與擴(kuò)展方針則是創(chuàng)建多態(tài)性行為類,用于取代直接通過$_GET調(diào)用函數(shù)??偠灾?,對(duì)于這一范例的改進(jìn)可謂無窮無盡,有興趣的朋友可以嘗試將自己的想法轉(zhuǎn)化為現(xiàn)實(shí)。我在附件的GoogleCalObjectOrientedFinal文件夾中保存有代碼的最終版本,大家能夠以此為起點(diǎn)進(jìn)行探索。

如果大家的好奇心比較強(qiáng),也可以試著將這款小應(yīng)用與其它日程表服務(wù)對(duì)接,看看如何在不同平臺(tái)上以不同方式實(shí)現(xiàn)信息輸出。對(duì)于使用NetBeans的朋友,每個(gè)源代碼文件夾中都包含有NetBeans項(xiàng)目,大家只要直接打開即可。在最終版本中,PHPUnit也已經(jīng)準(zhǔn)備就緒。不過我在其它項(xiàng)目中將其移除了——因?yàn)檫€沒有經(jīng)過測(cè)試。

感謝您的閱讀。

附件下載地址:https://github.com/tutsplus/From-Procedural-to-Object-Oriented-PHP

原文鏈接:http://net.tutsplus.com/tutorials/php/from-procedural-to-object-oriented-php/

責(zé)任編輯:林師授 來源: 51CTO
相關(guān)推薦

2016-04-19 14:29:53

中國金融交易大會(huì)

2014-06-11 13:47:54

廣告技術(shù)

2014-02-13 15:38:12

移動(dòng)廣告

2009-09-10 15:41:58

PHP Java面向?qū)ο?/a>

2014-05-28 17:35:35

程序

2018-11-13 15:14:07

2014-03-05 16:37:20

移動(dòng)廣告

2014-06-12 10:32:27

易傳媒品牌營銷

2009-09-22 12:25:04

ibmdwDB2

2015-01-19 11:45:10

2009-04-23 14:53:35

2018-07-16 16:10:03

前端JavaScript面向?qū)ο?/a>

2023-01-10 09:38:09

面向對(duì)象系統(tǒng)

2009-11-23 19:24:01

PHP面向?qū)ο缶幊?/a>

2016-11-10 20:18:16

Header Bidd程序化交易廣告

2011-07-05 15:22:04

程序設(shè)計(jì)

2010-02-26 14:40:15

Python應(yīng)用程序

2011-07-05 15:59:57

面向?qū)ο缶幊?/a>

2009-06-16 17:09:17

Scala面向?qū)ο?/a>函數(shù)編程

2021-02-23 12:25:26

鴻蒙HarmonyOS應(yīng)用開發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)