文章目錄
- Objects - 對象
- Abstraction - 抽象
- Encapsulation - 封裝
- Polymorphism - 多態
- Inheritance - 繼承
- Singleton - 單例
- Decorator - 裝飾
- Observer - 觀察者
- Bridge - 橋接
- Chain of Responsibility - 責任鏈
- Command - 命令
- 需要改進的方面
原文連結 Drupal programming from an object-oriented perspective
目前無論在 Drupal 的原始碼還是 API 介面中我們都看不到使用 OOP 的直接證據(例如: 在一些明顯應該使用 class 的地方沒有看到 class 的定義), 這為 Drupal 招來了很多的批評, 被認為是 Drupal 一個不合潮流的缺點. 的確 Drupal 沒有直接使用 PHP 的 OOP 特性, 但是在 Drupal 的設計中還是應用了很多 OO 的設計原則. 本文從面對對象的視角來描述 Drupal 的系統架構, 以便 OOP 程式員能夠從 OO 設計原則的角度上對 Drupal 做出正確的評價, 並希望通過本文使讀者對 Drupa 有更深入的瞭解l. 今後隨著 PHP 和 Drupal 的逐漸成熟, Drupal 會越來越多的引入 PHP 的 OOP 特性.
當前設計的動機和原因
自從 Drupal 發布 4.6 版本以後, Drupal 就決定不再使用 PHP 的 class , 做出這個決定是源自如下幾個理由:
首先, 最初設計 Drupal 的時候大家都還在使用 PHP 4 , 那時 PHP 4 對 OO 的支援還很不成熟, 直到 PHP 5 發布這種情況才得到明顯的改善.
其次, PHP 在載入大量代碼時效能會大大降低, 這在沒有 PHP accelerator(Zend) 時表現的尤其明顯: 在測試過程中 PHP 編譯 Drupal 頁面原始碼的時間佔到整個處理過程的一半以上. 為瞭解決這個關鍵性問題 Drupal 使用了"延時載入", 使每一次頁面請求都只載入真正需要的那部分代碼. 這種方法在 Drupal "模組-函數-HOOK" 模式下工作的很好, 但是 PHP 不支援在 class 中的應用. 這就導致要麼忍受"慢(載入大量定義了 class 的 php 代碼)", 要麼忍受"亂(在 index.php 中編寫大量的邏輯代碼以減少代碼載入量)", 顯然這都是不能接受的.
最後, 有些 OOP 設計原則是在 Drupal 準備要使用的時候發現 PHP 不支援或者是當時沒有被支援. 例如, Drupal 在 theme system 中想使用一種類似於 Objective-C's "categories", (Ruby - 'open classes', Javascript - 'modifying object prototypes', C# - 'extension methods'). 這個想法的核心就是 class 能夠被多次定義, 你可以在不同的地方定義同一個類. 這樣就允許在不修改原始定義的情況下擴充一個類的功能, 非常符合 OOD 中"開-閉"原則即"對修改關閉對擴充開放".
在即將到來的 Drupal 7 中會有一些改變: 首先 Drupal 7 會強制要求使用 PHP 5 這會給引入 OOP 帶來很多的便利, 還有就是在一些模組中開始使用 class. 但 Drupal 的核心結構依然不會使用 class.
Drupal 中的 OOP
儘管在 Drupal 中沒有明確的使用 class, 但是在 Drupal 的設計中還是使用了一些 OO 的特徵. 我們可以依照 OO 的規範在 Drupal 的設計中找到 在一個 OO 設計系統中必不可少的特徵.
Objects - 對象
Drupal 中有很多元素是符合 OOP 規範中關於"對象"的描述的. 其中能夠比較明顯的被看成是"對象"的是: 模組(modules), 主題(themes), 節點(nodes)和使用者(uses) 這幾個組件.
節點在 Drupal 網站中是最基礎的資訊構件, 在一個典型的資訊發布系統中它代表了所有類型的資訊資料. 通常系統會通過函數 node_invoke() 調用定義在 node.module 中功能方法. "使用者"對象則包含了網站帳號資料, 使用者的自訂資料和 Session 資訊. 這些對象的資料結構都使用資料表代替 class 定義在資料庫中. 由於 Drupal 使用的是關聯式資料庫, 這就允許其他模組採用附加資料的方式擴充這些對象.
模組和主題也象是一個對象, 在很多方面它們都充當著"控制者"的角色. 模組不僅僅代表一個源檔案, 它還是一個相關函數的集合并且在 Drupal 定義的 Hook 系統中重要的組成部分.
Abstraction - 抽象
Drupal 的鉤子系統(或者叫回調系統)可以看作一種介面實現. "鉤子"定義了能在模組上或被模組執行的操作, 如果模組實現了一個鉤子, 就相當於締結了一個執行特定任務或返回特定資訊的契約. 調用方並不需要知道模組和鉤子實現細節就可以調用鉤子.
Encapsulation - 封裝
Drupal 在封裝特性的實現上沒有採用嚴格的"存取控制", 它採用的是"命名規範". Drupal 的代碼是基於函數的, 所有的函數都在一個命名空間下, 系統可以通過函數名字中的首碼劃分出子空間. 通過簡單的"命名規範"模組可以定義沒有衝突的私人函數和變數.
命名規範同樣可以從一個模組內的函數中定義出公用介面. 私人函數的名字前面有一個"_"做首碼, 外部模組是不應該訪問另一個模組中這樣命名的函數的. 例如: _user_categories() 是一個私人函數, 從外面直接存取它是不合約定並且很不安全的, 它的任何變化是沒有預先通告的. user_save() 是 user 模組的公用介面(API), 調用這個函數就可以很安全的把使用者物件儲存到資料庫中.
Polymorphism - 多態
通常情況下"節點"是有多種形態的(常見的有 Page 和 Story), 如果一個模組要在頁面上顯示一個"節點", 首先要調用這個"節點"上的 node_build(), 然後調用 drupal_render() 得到包含"節點"內容的 HTML代碼. 實際的渲染過程(得到 HTML 程式碼的過程)依賴於傳入"節點"的類型, 這一點很像一個類型對象根據接受到的訊息來決定自己的行為. Drupal 內部實現了通常在 OOP 語言運行庫中完成的任務.
此外, 在上面的例子中"節點"的渲染還會受到當前主題的影響. "主題"的外觀效果是多種多樣的但是介面是不便的, "主題"通過處理"渲染節點"訊息給"節點"附加上特定的外觀效果, 而具體的視覺效果取決於當前主題的實現.
Inheritance - 繼承
模組和主題可以被看成從"抽象類別"中繼承的類型. 就"主題類"來說"抽象類別"的方法定義在 theme.inc 中, 預設實現是由系統中啟用的模組提供的, 自訂的主題可以重寫任意一個介面組件的顯示. "模組"也是一樣, "模組"的 HOOK API 在實現時都是可選.
Drupal 中的設計模式
Drupal 的內部結構大多很複雜, 使用繼承和多態這樣的簡單設計已經不能夠很好的處理它們了. 那些令人興奮的系統特性都是應用了成熟的設計模式的結果. 很多在<設計模式>("Gang of Four"的書)中詳細描述的設計模式都在 Drupal 中得到了應用, 例如:
Singleton - 單例
如果我們把"模組"和"主題"都看成對象的話, 那麼這些對象都應用了單例模式. 一般情況下這些"對象"都不包含資料, 區分它們的方法是看它們包含的函數, 這就可以把它們看成只有一個執行個體的類.
Decorator - 裝飾
裝飾模式在 Drupal 中應用非常廣泛. 在我們前面討論中我們提到過 node 對象具有多種形式, 但這隻是 node 系統的一小部分功能. 更加引人注意的是任何模組都可以通過實現 node hook (例如: hook_node_load(), hook_node_view()) 來擴充所有 node 對象的功能.
利用這個特性可以在不進行"子類化"的情況下為 node 擴充出多種功能. 例如, Drupal 內建的新聞報道節點(story node)只包含標題, 作者, 內容摘要,本文等很少幾種屬性. 現在需要為它添加一個常用的功能-附加檔案, 一種方式是設計一個新的節點類型,新類型將包含新聞報道的全部功能並增加附加檔案功能. Drupal 的上傳模組則使用一種更加模組化, 更簡潔也更強大的方式實現了這一需求, 它通過使用 node API 實現了可以為任何一種節點類型添加附加檔案功能.
Drupal 上傳模組的實現很類似於使用 Decorator 模式包裹每個節點對象. 在支援 categories 特性的 Objective-C 語言中可以很簡單的通過擴充 node 基類的功能, 來為所有的 node 對象添加功能. Drupal 通過 hook 系統, 調用 node_invoke() 函數實現了簡化的效果.
Observer - 觀察者
上面關於 node 對象功能擴充的示範也很類似於在 OO 系統中使用觀察者模式. 觀察者模式可以說在 Drupal 中無處不在, hook 系統從本質上講就是允許模組註冊成為 Drupal 對象的觀察者. 例如: 當在 Drupal 的分類系統中修改詞彙表時, hook 系統就會去調用所有實現了hook_taxonomy_vocabulary_update() 的模組. 通過實現分類系統的 hook 模組註冊成為詞彙表的觀察者, 有關詞彙表的任何修改都會在恰當的時候通知觀察者.
Bridge - 橋接
在 Drupal 的資料庫抽象層的實現方式上多少有些橋接模式印記. 編寫模組時要使用資料庫抽象層訪問資料, 避免使用特定資料庫的特有功能. 這樣變更資料庫時就是不需要修改現有模組的代碼.
Chain of Responsibility - 責任鏈
Drupal 的菜單系統在設計時遵循了責任鏈模式. 對於每個頁面請求, 菜單系統都要判斷是否有模組處理這個請求, 目前使用者是否有許可權訪問被請求的資源, 處理這個請求需要調用哪個函數. 為了做到這些系統會把請求路徑發送給功能表項目, 如果當前的功能表項目不能處理這個請求, 它就被發往責任鏈的下一環, 直到有模組處理了這個請求或者是有模組拒絕了目前使用者的訪問或者是責任鏈沒有後續環節, 這時請求的處理才會結束.
Command - 命令
許多 Drupal 的 hook 使用命令模式減少必須實現的函數數量(除了傳遞正常的參數還把要求的操作作為參數傳入). 事實上, hook 系統本身也是用命令模式來支援模組只為它們關注的操作定義 hook.
為什麼不使用 Classes.
希望在這裡能夠表述清楚 Drupal 中實現 OOP 概念的方式. 那麼, 為什麼 Drupal 不傾向於在今後使用 classes 來代替現有設計呢? 一些原因是曆史遺留問題, 更重要的是既然我們有辦法在 Drupal 應用 OO 設計模式, 那為什麼一定要使用 classes.
一個很好的例子是關於主題系統的擴充性, 如果想實現一個新的介面主題, 那麼就 OOP 來說很自然的想法是從預設主題類裡繼承一個新類來實現. 當新主題需要支援一個基類不支援的介面元素時,就沒有簡單的辦法可以使用了. Drupal 的主題系統使用函數分發系統簡潔優雅的解決了這個問題. 在類似這樣的情況下, classes 從表面上看使系統變的簡潔清晰, 但這也讓系統變得僵硬難以擴充.
需要改進的方面
雖然 Drupal 設計中做了很多面多個物件的實踐, 但是在一些方面還需要加強.
封裝, 雖然在設計上做的很充分, 但是在代碼編寫的時候並沒有得到很好的貫徹執行. 本應該嚴格設計模組的公用函數和私人函數, 但在執行過程中更傾向於公開大部分函數, 即便有些函數不得不經常變動. 而且 Drupal 更新時的向後相容策略使這個問題日益嚴重. 這個向後相容的策略需要對封裝做更好的執行才能給系統帶來更好的收益.
繼承, 在系統中的應用並不多, 就如上面說的所有的模組共用公用介面, 它很難擴充到新模組中. 用一個新模組擴充一個存在的模組的功能很容易, 但是沒有辦法重寫某個模組的方法. 我們可以把大的模組分解成小的功能函數庫來減少模組給系統帶來的問題.