最近更新: 2008-03-27

延續《分割程式功能以及 mix-in 和 include》的討論

延續《分割程式功能以及 mix-in 和 include》的討論。tokimeki 說: 既然已經在外部定義了function,直接調用function不就好了嗎?

我直接委派函數的原因是 PHP 語法限制。用函數比用靜態成員函數(類別方法)或一般成員函數(個體行為)要容易寫。請看我在另一篇的回應: 這牽涉到 PHP 的動態能力限制。

至於不直接調用 function 這點,那是因為直接調用 function 的方式將損失動態性,或者說 "彈性"。如下例:

//--------
function foo() { echo 'foo'; }
function bar() { echo 'bar'; }

class A extends MixableClass {
}
class B {
  function say() { echo 'BBB'; }
}

function allSay($a) {
  foreach ($a as $o) {
    $o->say();
  }
}

//--------
$a = array(new A, new A, new B);

$a[0]->say = 'foo';
$a[1]->say = 'bar';

allSay($a);

/*
foo();
bar();
$a[2]->say();
*/

以這例子來說,我只需要知道我要調用 say 行為,其他都不用管了。如果直接調用函數,哪要如何寫?把 allSay($a); 這行改成沒有彈性的三行?

tokimeki又說: 我所需要的是「擴充」該類別的功能,換言之,他必須能像繼承一般,可以調用類別內protected的方法

先看繼承,我的 MixableClass 的設計目標就是要能繼承,這點並沒有什麼問題。至於說到擴充 protected method 的事,Ruby 可以做到這一點,但 PHP 不行。以 PHP 的動態能力來看,不允許你混入 protected 或 private method。

Ruby 可以 'open class' 後改變原有類別的任何方法 (包括 protected method)。如下例:

class A
  def say
  end
  protected :say

  def run
    self.say
  end
end

#open class A
class A
  def say
    print 'hello'
  end
  protected :say
end

a = A.new
a.run

第20行的 a.run 可以證明上例並不是定義一個新的 A 類別覆蓋先前的 A 類別。因為第2段 class A 的定義中,並沒有提到 run 這個行為。如果是覆蓋而非 'open' ,那麼就不會有 run 行為。

想學 Runy 這麼做嗎? PHP 會告訴你發生重覆定義類別的錯誤。

或許我們可以在 MixableClass 的 prototypeDelegate() 中多加一個指定存取屬性的參數,然後在 __call() 中排除嘗試調用註明 protected 的混入方法。不過 PHP 的動態能力不足以應付這種情形: 我們不知道是誰 call? 別忘了個體調用自己的行為時,也會透過 __call() (當它原本沒有定義時)。

按 PHP 目前的能力來看,想要達成你的需求,要嘛用 include ,要不就用 hook 的技巧:你預先在方法定義中埋入 hook points ,然後你在外部插入 hooks 。

class A {
    protected static $hooks = false;

    function __construct() {
        if ( !self::$hooks ) {
            self::$hooks = array();
            foreach ( get_class_methods(get_class($this)) as $methodName ) {
                self::$hooks['pre_' . $methodName] = create_function('', 'return true;');
                self::$hooks['post_' . $methodName] = create_function('', 'return true;');
                echo $methodName, "\n";
            }
        }
    }

    protected function say() {
        call_user_func(self::$hooks['pre_say']);

        echo 'hello';

        call_user_func(self::$hooks['post_say']);
    }

    public function run() {
        return $this->say();
    }
    /*
    ...
    其他省略,各位應該知道怎麼做了吧?
    */
}

$a = new A;
$a->run();
樂多舊網址: http://blog.roodo.com/rocksaying/archives/5763473.html

樂多舊回應
HACGIS@gmail.com(tokimeki) (#comment-16108325)
Sat, 29 Mar 2008 00:43:33 +0800
我試著整理一下我的論點:
1. 我的目的是為了分割程式的功能,也就是說,原本調用的方式是$x->y();,分割完後還是用$x->y();。

2. 我並不是為了想取得更多的動態性,反而我個人是認為過多的動態可能會引起混亂,這也就是為何我只用__call而不用__set的原因。

3. 每個插件對於他的宿主的非私有方法擁有直接調用的能力,反過來也是成立的。唯一的例外是有同名方法情況時,會根據本身物件具有的方法為優先,這點在以宿主物件調用方法時,保障了插件不會覆寫宿主原有的方法,這件事對於插件本身的地位來說是至關重要的。
HACGIS@gmail.com(tokimeki) (#comment-16108363)
Sat, 29 Mar 2008 00:48:58 +0800
另外我覺得,不論我們設計的機制或是想達到的目的為何,既然都不能免除使用__call加上函數表來達到我們要的目的,那麼效率的低落是在所難免。
令人欣慰的事情是,這樣的作法可以讓我們在「個人」可行的範圍內,減少我們維護程式碼的負荷。

不過最後我還是要文人相輕一下:我不喜歡用eval,也不喜歡用反映,我個人是覺得那個真的算是奇技淫巧,未來PHP改版後能不能適用,其實我不太有把握之後的相容性。
HACGIS@gmail.com(tokimeki) (#comment-16222845)
Tue, 15 Apr 2008 00:50:37 +0800
本來這個系列應該到此結束,但最近的實做跟重構過程,我發現幾個問題,想跟你請教:
1. 用這類方式擴充類別的能力,有時會碰到相依性的問題,當然我可以複寫建構式來先行載入相關的「插件」,可是當某個插件被使用的機率很高時,我是否該改寫他,將他重構進入「宿主」類別中呢?
2. 有時我需要某些橫切的機制,比如說紀錄、快取等等,但有時候這些機制會跟我選擇的儲存體相關(檔案、資料庫),這些又整個糾結在一起(比如我把紀錄放在資料庫,那如果資料庫存取發生錯誤怎麼辦),有沒有一個比較清晰的方式可以解決?
未留名 (#comment-16262559)
Mon, 21 Apr 2008 14:19:58 +0800
1.如果是 Ruby/JavaScript/Python 等等動態語言開發的專案,那是完全不需要考慮 "重構進入宿主" 這種事的。因為不需要這麼做。

你在先前的回應中提到了"函數表",事實上,基於個體導向的動態語言本身就是一個巨大的函數表管理器。所謂的建立類別與方法等動作,就是重覆的插入、置換函數表。因為它的內部機制就是如此,我們是沒有必要考慮 "重構進入宿主" 這種事的。

但對於 PHP 而言,因為它在這方面帶有 Java 的影子(說實話,這真是個錯誤決策),所以你可能需要這麼做。我會建議你先做效能測試,如果 "載入機制" 會造成瓶頸,那就把它寫進去。

2.我的習慣是,當我調用一個動作失敗時,就回傳 false 或丟出例外訊息,交給 Container 去處理。我看不懂你想要尋求什麼?
HACGIS@gmail.com(tokimeki) (#comment-16281703)
Thu, 24 Apr 2008 09:32:56 +0800
感謝你的建議,我會量測看看兩種作法的時間差異