物件導向程式設計
物件導向程式設計 (Object-Oriented Programming) 至今已經有半個世紀的歷史,它的誕生無非是為了解決程式錯綜複雜的結構設計,它的出現確時大幅提昇程式碼的維護性。物件導向大概有寫過程式的人都聽過,路上隨便抓個會寫程式的大學生,問他什麼是物件導向,比較制式的答案當然就是「封裝 (Encapsulation)、繼承 (Inheritance) 與多型 (Polymorphism)」。但實際上在我們使用物件導向撰寫程式碼時,除了靈活運用封裝、繼承與多型等特性之外,還會運用一些設計模式 (Design Pattern) 來設計系統。有了物件導向為什麼還需要「設計模式」呢?其實這只是為了幫助我們設計出更容易維護的系統,避免落入物件導向的依賴陷阱。
依賴關係是物件導向的不明產物
我們都知道系統的耦合關係是越鬆散越好、越低越好,一個系統最好可以很輕易的切割功能與新增功能。系統耦合性有些抽象,講起來很簡單,做起卻不容易。先講個故事先:
A 程式設計師:我負責的系統最近需要計算腳踏車的速度,這程式可要花上不少時間!
B 程式設計師:哈,A 兄,你賺到了!正巧我上個月才寫完計算機車速度的程式,你拿去用就好,更好的消息是,我用了物件導向,你只要將某某類別 (Class) 實體化之後就可以使用了!程式在這 ... 你整合進去就可以用囉!
A 程式設計師(取得程式後開始整合到自己的系統,編譯時發現這個類別繼承了其他類別 ... GG)
A 程式設計師:感謝 B 大大無私的分享,但我編譯的過程中似乎少了某某類別,可以補給我嗎?
B 程式設計師:哈,我漏了,繼承的類別在這 ... 你應該整合進去就可以用囉!
A 程式設計師(取得繼承類別後開始整合到自己的系統,編譯時發現這些類別還有實做一些介面 ... GG)
A 程式設計師:再次感謝大大無私的分享,但我編譯的過程中似乎少了某某介面的程式碼,可以再補給我嗎?
B 程式設計師:哈,我又漏了,介面程式在這 ... 這次是真的!
A 程式設計師(取得介面後程式編譯還是失敗,因為又發現某些類別使用了其他類別)
A 程式設計師:又 GG 了,好像少了一堆類別與介面等等 ...
B 程式設計師:我整個系統程式都給你好了
A 程式設計師(用很奇怪的方法整合了 B 兄提供的系統,最後因為類別名稱衝突,還是失敗了 ... 最後只好違反 DRY 原則 (Dont Repeat Yourself) Copy 程式碼來達成 ...)
怪了,物件導向不是很好維護嗎?怎麼會一直發生這些蠢事呢?答案其實就是「依賴關係」搞的鬼,我們在設計與實作時忽略了依賴關係這個因子,那什麼是依賴關係呢?其實我們的程式碼中,依賴關係處處可見,對於程式來看所謂的依賴就是「沒有你我活不下去」。先撇開物件導向的實作原則等等裡論,我們來看看「依賴關係」究竟藏在哪裡,先看看以下程式碼範例 (請容許我用不是很物件的 PHP 來說明):
<?php interface Car { // interface.. } interface Controllable { // interface.. } class TwoWheelCar { // implements... } class BicycleDriver { // implements... } class ManPower implements Power { // implements... } class Bicycle extends TwoWheelCar implements Controllable { private $_driver = null; private $_power = null; public function __construct () { $this->_driver = new BicycleDriver(); $this->_power = new ManPower(); } // implements.. } class Story { public static function main () { new Bicycle(); // do something ... } }
上述的的程式中我們先觀察一下 Bicycle Class 的程式碼,分析其依賴關係如下:
- Bicycle Class 繼承 TwoWhellCar Class,沒有 TwoWhellCar Class 活不了
- Bicycle Class 實作 Controllable Interface,沒有 Controllable Interface 活不了
- Bicycle Class 實體化與使用 BicycleDriver Class,沒有 BicycleDriver Class 活不了
- Bicycle Class 實體化與使用 ManPower Class,沒有 ManPower Class 活不了
上述可見 Bicycle Class 依賴了許多其他的類別與介面,然而在實際的編譯與執行過程中,依賴關係更是深遠,就像葡萄一樣抓起來一整串。在傳統的物件導向程式語言中,只要有「繼承、實作與實體化類別」這些地方就會產生依賴關係。在物件導向的設計理念中,類別除了繼承與實作產生了依賴關係,在程式碼中直接依賴其他類別是比較不被推薦的,然而靜態類別的使用更是容易不小心就產生了依賴關係。
利用設計模式改善類別依賴關係
回顧上述所產生的依賴關係,這結果將使得 Bicycle Class 的可攜性變得很低。但是有些地方所造成的類別依賴,是可以透過一些技巧來改善的,像是實體化類別的動作。我們修改一下 Bicycle Class 與 Story Class 如下:
<?php interface Car { // interface.. } interface Controllable { // interface.. } interface Driver { // interface.. } interface Power { // interface.. } class TwoWheelCar implements Car { // implements... } class BicycleDriver implements Driver { // implements... } class ManPower implements Power { // implements... } class Bicycle extends TwoWheelCar implements Controllable { private $_driver = null; private $_power = null; public function setDrive (Driver &$driver) { $this->_driver = $driver; } public function setPower (Power &$power) { $this->_power = $power; } // implements.. } class Story { public static function main () { $bicycle = new Bicycle(); $bicycle->setDrive(new BicycleDriver()); $bicycle->setPower(new ManPower()); } }
在上述的程式碼中,我們主要修改了兩個地方:
- 改用 Driver 與 Power Interface 取代原本 Bicycle Class 直接依賴 BicycleDriver 與 ManPower Class
- 將原本 Bicycle Class 實體化的工作轉交由第三方 Story 處理
第一點改用 Interface 拆解依賴關係是物件導向比較常見的技巧,這裡就不多做說明。第二個動作比較值得思考,我們將實體化類別的動作較給其他人處理,增加 Story 這個衰人的依賴關係,讓 Bicycle 自身變得更清澈,這樣轉移實體化動作到另一個衰人的技巧我們稱為依賴注射 (DI, Dependency Injection)。這樣一來 Bicycle Class 就剩下繼承類別與幾個介面存在依賴關係,可攜性變高了。可是想想,不對啊,如果大家都把這樣的依賴關係轉移到 Story Class,這樣一來 Story 可就衰了!但其實在實務上,我們會利用更高級的解依賴技巧,像是反射、動態實例化、混成、註譯設計甚至動態編譯等等手段來達成,今天先到這裡,關於依賴注射的實作我們下次在介紹囉。