利用 NullObject 改善程式可讀性,No more if, no more try
剛在重構一組類別的程式碼時,突然想到 Martin 在《敏捷軟體開發原則、樣式與實務》一書中提到的一個編程技巧,就是在失敗狀況時回傳 NullObject ,避免行為調用者用 if
或 try
處理失敗狀況,影響程式可讀性。
我重構中的類別程式碼,基本上是一個聚合類別,它包含了其他類別的個體。此聚合類別提供一個方法 get()
,以取得它所包含的個體。外部調用 get()
後取得內容個體後,立即呼叫該個體的一個方法。
原本的程式碼,其架構大略如下所示。
<?php
class A {
function output() {
echo 'hello world';
return $this;
}
}
class C {
var $objects;
function __construct() {
$this->objects['a'] = new A;
}
function get($name) {
return (isset($this->objects[$name])
? $this->objects[$name]
: false
);
}
}
$c = new C;
$c->get('a')->output();
$c->get('b')->output();
//=========
if ($o = $c->get('a'))
$o->output();
if ($o = $c->get('b'))
$o->output();
//=========
?>
程式碼的語意是調用 $c->get()
取得內容個體,接著調用取出物的 output()
行為。問題在於這段程式碼並未考慮找不到指定內容個體的情形,故第27行的 $c->get('b')->output()
將會引發調用不存在個體之方法的執行錯誤。直覺上,我們的解決的方式就是修改調用行為的這一段程式碼,加上 if
或 try
的錯誤處理流程。
但這在大型專案或分工團隊中可能會有副作用。例如甲是負責基礎類別的程序員,也就是負責寫上例第1到23行程式碼的人;乙是負責應用流程的程序員,也就是寫第24到27行程式碼的人。現在甲要重構基礎類別,並可能改變行為回傳結果時,按直覺的方式,我們將牽連乙去修改他的程式碼。這就是一種不良副作用。
而 Martin 所說的編程技巧,則是在碰到找不到指定個體時就回傳一個 NullObject,使得調用者可以不用修改程式碼。這個編程技巧用 PHP 實作很簡單,只要利用 PHP5 的 magic method 就能輕鬆實現。如下所示。
NullObject and magic method
<?php
// PHP5 magic style
class NullObject {
function __call($name, $args) {
//do nothing
return $this;
}
}
class A {
function output() {
echo 'hello world';
return $this;
}
}
class C {
var $objects;
function __construct() {
$this->objects['a'] = new A;
}
function get($name) {
return (isset($this->objects[$name])
? $this->objects[$name]
: new NullObject
);
}
}
$c = new C;
$c->get('a')->output();
$c->get('b')->output();
?>
我們重構時,增加一個 NullObject 類別,這類別就是什麼事都不做。所以我們再利用 magic method 的 __call()
實作一個什麼都不做的泛用行為。這可以免除 PHP 發出找不到指定行為的執行錯誤。最後,我們只要再把 get()
回傳 false 的動作改成回傳 NullObject 即可。
就這樣,我們完成了重構動作。而調用者的程式碼不必進行任何修改,就無聲無息地略過了錯誤處理流程並保持程式碼簡潔的語意。
Java style
附帶一提。這個編程技巧也可以用來檢視程式語言的動態性對我們的影響。假如我們不用 PHP5 提供的 magic method ,那麼我們就要採用類似 Java 的編程風格來重構,範例如下。
<?php
// Java style. (Are you sure that you are using PHP5?)
interface I {
function output();
}
class CNullObject implements I {
function output() {
//do nothing
return $this;
}
}
class A implements I {
function output() {
echo 'hello world';
return $this;
}
}
class C {
var $objects;
function __construct() {
$this->objects['a'] = new A;
}
function get($name) {
return (isset($this->objects[$name])
? $this->objects[$name]
: new CNullObject
);
}
}
$c = new C;
$c->get('a')->output();
$c->get('b')->output();
?>
在 Java 編程風格下,我們需要先定義一個具有 output()
行為的介面I。再為 C 類別專門定義一個實作了介面I 的 CNullObject 類別。
這種受限於程式語言動態能力所帶來的編程風格,最顯而易見的累贅就是要定義許多 xNullObject,而它們所作事都一樣: 不做任何事。結果我們為了不做任何事的類別複製了許多重複的程式碼。
樂多舊回應