Skip to content
ツ Liverbool edited this page Aug 18, 2013 · 23 revisions

ตัวอย่างการสร้าง Observer Pattern (PHP)

Observer Pattern เป็นหนึ่งใน pattern ที่ใช้กันมากบน Framework ต่างๆ (ไม่ว่าจะภาษาใดก็ตาม) เนื่องจากเป็น Pattern ที่ตอบโจยท์เรื่อง การแก้ไข/เพิ่มเติมความสามารถของ Core Framework โดยไม่ต้องเข้าไปแก้ไขใน Core จริงๆ หรือที่เรียกว่า Hacking

Open Source Framework ก็ต้องเป็น Black-Box

แม้ว่า Framework หลายๆ ตัว โดยเฉพาะ PHP Framework จะเป็น Open source ก็ตาม การที่เราจะแก้ไขหรือเข้าไปเพิ่มเติมความสามารถบางอย่างใน code โปรแกรมโดยตรงนั้น ถือว่าไม่ใช่วิธีที่ถูกต้อง เพราะแม้จะเป็น Open source เราก็ยังต้องมอง Framework ต่างๆ ในลักษณะของ software แบบ Black-box (กล่องดำ) ที่ไม่สามารถ (ไม่ควร) เข้าไปแก้ไขอะไรเลยอยู่ดี เนื่องจากว่าหากฝั่งผู้พัฒนา Framework นั้นๆ ออกซอฟต์แวร์เวอร์ชั่นใหม่มา ย่อมส่งผลกระทบต่อส่วนที่เราเข้าไปแก้ไขไว้แน่นอน

ทางออกคือ ใช้อย่างเดียว อย่าไปแก้ไขอะไร ยกเว้นจะจำเป็นจริงๆ ซึ่งคำว่าจำเป็นจริงๆ ก็คือ มันจำเป็นจริงๆ ​:)

ทำไมเราถึงจะต้องไปแก้ไข Core Framework ด้วยล่ะ?

ถ้าซอฟต์แวร์ตัวไหนที่เป็นแค่ Framework เพียวๆ เราก็คงไม่มีความจำเป็นอะไรต้องไปแก้ไข core โปรแกรม (ถ้าไม่จำเป็นจริงๆ หรือมันเป็น Bug) เนื่องจากเราจะใช้ Framework ตัวนั้นมาสร้างเป็น โปรแกรมสำเร็จรูปของเรา อีกทีนึง แต่ถ้ามันเป็น Software สำเร็จรูปมาแล้ว เช่น WordPress, Joomla หรือแม้กระทั่ง Windows Live Message รวมถึง Framework ในระดับบนหรือ Hi-Level Framework หลายๆ ตัวเช่น Sencha (JavaScript) ก็คงจะมีหลายส่วนที่เราอยากจะเขียนเพิ่มเติมเข้าไป หากต้องใช้มัน เราอาจจะ:

  • อยากปรับเปลี่ยนบางส่วนของมัน
  • เพิ่มความสมารถบางอย่างเข้าไป
  • สร้างโปรแกรมให้สามารถเปิด/ปิด ความสามารถบางอย่างได้ (Plugins)
  • ไม่สะดวกที่จะใช้วิธี extends/inherit เหล่านี้คือเหตุผลในหลายๆ อย่าง ที่เราจำเป็นต้องอยากแก้ไข/เพิ่มเติม Core Framework และวิธีการของ Observer Pattern สามารถช่วยเราแก้ปัญหานี้ได้ และการใช้ Observer Pattern นี้ยังเป็นเครื่องมือสำคัญในการโปรแกรมมิ่งเชิง Event-Driven อีกด้วย

องค์ประกอบของ Observer Pattern

ประกอบไปด้วย Class 2 ตัวหลักคือ

  +--------------------+                 +---------------+
  |     Subject        | <>------------- |   Observer    |
  +--------------------+                 +---------------+
  | + ObserverAdd      |                 | + Update      |
  | + ObserverRemove   |                 +---------------+
  | + ObserverNotify   |
  +--------------------+

  - Subject บางทีเรียกว่า Observable, Plugable เป็นต้น ความหมายคือ Object คอยเก็บหรือรับสมัคร objects อื่นๆ ไว้
      + ObserverAdd       บางทีเรียกว่า add, addListener, addObject, subscribe, register...
      + ObserverRemove    บางทีเรียกว่า remove, removeListener, removeObject, unsubscribe, unregister ...
      + ObserverNotify    บางทีเรียกว่า fire, fireEvent, trigger, triggerEvent, notify ...

  - Observer คือ object ที่สร้างขึ้นเพื่อเข้าไปจัดการ (แก้ไข/เพิ่มเติม) บางส่วนใน Observable Object หรืออาจจะเรียกว่า Handler Object ก็ได้
      + Update  คำว่า update ในที่นี้เป็นตัวแทนความหมาย method ที่จะถูกเรียกใช้งาน ตามการค้นหาของ ObserverNotify โดยมากจึงเป็นชื่อของ Event ต่างๆ
        แปลว่าในความเป็นจริง method นี้ไม่ได้ชื่อว่า update แต่อาจจะชื่อว่า onBeforLoad, onAfterLoad, onLoad ...

  * ความหมายของ <>---------- หมายถึง ความสัมพันธ์ที่ขึ้นตรงต่อกัน ขาดกันไม่ได้ (Aggregate) คือต้องประกอบกันให้ครบถึงจะทำงานได้จริงตามคอนเส็บ  
  ดูเพิ่มเติม http://alan-amin.net/uml.pdf

ตัวอย่าง Observer Pattern - PHP

/* ลักษณะ class โดยรวม */
interface Observer {
    function Update();
}

interface Observable{
    function ObserverAdd(Observer $Observer);
    function ObserverRemove($ObserverKey);
    function ObserverNotify($NotifyName, $Args);
}

/* class ที่ใช้งานจริง สมมติว่าเราจะเขียนโปรแกรมให้มี Plugin ได้ */

/**
 * @name Plugable class ที่สืบทอดจาก Observable จะเป็น class ที่อยู่ในส่วนของ Application ภายใน
 *                โดยส่วนมาก class นี้จะสร้างไว้เพื่อให้ class อื่นมา inherit อีกที
 */
class Plugable implements Observable {
   /**
    * สร้างตัวแปรสำหรับเก็บ  object server ต่างๆ เสมือนเป็น Server
    */
    private $_servers = array();

   /**
    * เก็บ Object Server หรือ Handler Object ไว้ใน Server
    * 
    * @param    [Observer object] Object ที่สืบทอดจาก Observer class
    */
    function ObserverAdd(Observer $Observer){
       /**
        * ในขั้นตอนนี้อาจจะต้องตรวจสอบก่อนว่ามีการเก็บ observer ตัวนี้หรือยัง เพื่อความชัวร์
        * การเพิ่ม server ส่วนนี้ข้อมูลจะเป็น array ควรจะจัดการด้วย class พวกที่ใช้จัดการ collection จะง่ายในการแก้ไข
        * ในที่นี้เราจะใส่ key ให้ array ด้วยเพื่อง่ายต่อการค้นหาภายหลัง โดยเอาชื่อ class observer เป็น key
        */
        $k = get_class_name($Observer);
        $this->_servers[$key] = $Observer;
    }

    /**
    * ลบ Object Server หรือ Handler Object ออกจาก Server
    * 
    * @param    [string] ชื่อของ Object Server (class name)
    */
    function ObserverRemove($ObserverName){
       /**
        * นี่คือวิธีการลบแบบง่ายๆ
        */
        if(isset($this->_servers[$ObserverName])
            unset($this->_servers[$ObserverName]);
    }

   /**
    * method นี้ใช้ในการค้นหา observer เพื่อทำการ notify, trigg observer นั้นๆ
    ​* โดยมากในการใช้งานจริงจะเป็นชื่อ event ซึ่งจะพ้องกับ ชื่อ method อันใดอันหนึ่งของ observer
    * จะเห็นว่าคอนเส็บคือ ทำการค้นหา method name ของ observer เพื่อทำการเรียกให้ทำงาน (call)
    * โดยที่ฝั่ง observable จะเป็นผู้ส่งพารามิเตอร์ไปให้ observer
    * ลักษระนี้เรียกว่าการ call-back ไปยัง observer เพื่อเรียกหาตัว handle
    * ผู้พัฒนาโปรแกรม (ฝั่ง observable) จึงต้องบอก (API) กับผู้ใช้ (ผู้เอาไปใช้) ว่าจะส่งพารามิเตอร์ใดไปให้บ้าง
    * เพื่อให้ไปจัดการแก้ไขต่อไป โดยสิ่งที่จะต้องส่งไปเสมอคือ obsevable ที่เป็นผู้ notify หรือผู้เรียก หรือเรียกว่า Sender
    */
    function ObserverNotify($EventName, $Args = null){
       /**
        * ค้นหา $EventName ของ observer ต่างๆ
        *
        * ในความเป็นจริงแล้ว observer (plugins) จะถูกเพิ่มเข้าไปใน server หลายตัว
        * และส่วนมากใน application หนึ่งตัวจะไม่ค่อยแยก event ออกเป็นส่วนๆ เท่าไร (ยกเว้นบางตัวเช่น Sencha)
        * การค้นหาจากชื่อ event อย่างเดียวอาจทำให้ผิดพลาดได้ วิธีการจริงๆ จึงต้องใส่ scope เพื่อระบุ observer ด้วย
        * จะทำการ scope ด้วย object หรือด้วย pre-fix, sup-fix ก็แล้วแต่เราออกแบบ
        * การ scope ด้วย object (sender) จะดีกว่าการ scope ด้วย pre-fix
        */
        
        /** สร้างตัวแปร results เพื่อเก็บผลลัพธ์จาก handler ซึ่งอาจต้องใช้ในบางกรณี **/
        $results = array();
        
        /** ค้นจาก observer ทั้งหมด **/
        foreach($this->_servers as $obj){
        	/**
	 	 * ตรวจหา method ของ observer object
	 	 * Observer->Update()
		 **/
            if(in_array($EventName, get_class_methods($obj)){
                /***
                 * ทำการ Notify, Trig, Update ไปยัง ​Observer
                 * ภาษา OOP เรียกว่า ส่ง message ก็คือ call method นั่นล่ะ (พูดตรงๆ)
                 * ขั้นตอนนี้จะมีการส่ง Sender ไปแบบ pass by reference (&$this) เพื่อแก้ไข Observable Sender
                 ***/
                $results[] = $obj->$EventName(&$this, $Args);
            }
        }
        
        return $results;
    }
}

class Plugin implements Observer{
    /** ไม่มีอะไรต้องทำ เพียงแต่เปลี่ยนชื่อให้สื่อความหมายกับความเป็นจริง **/
}

เริ่มใช้งานจริง

/**
 * class ฝั่งผู้พัฒนา class ไหนอยากให้มี event ก็ inherit จาก plugable ซึ่งเป็น Observable
 * CMS ที่เป็นที่นิยมโดยมากจะมีการเพิ่ม event หรือ สิ่งที่จะ notify ไปยัง subscriber (observer) ไว้อย่างครบครับ ทำให้ผู้พัฒนาต่อ
 * ในส่วนนี้เราอาจจะเพิ่ม (จริงๆ แล้วต้องเพิ่มเลยล่ะ เมื่อทำงานจริง) ส่วนที่เรียกว่า eventManager ขึ้นมา เพื่อกำหนด เหตุการต่างๆ ที่
 * เรามี และให้ผู้อื่นมาสมัครรับข่าวสารจากเหตุการนั้นๆ
 * มีความยืดหยุ่นในการปรับแต่ง แต่ยังไงก็ตามผู้พัฒนาก็ต้องออกแบบให้ดีว่าจะมี listener อะไรไว้บ้าง
 * ซึ่งการออกแบบให้ดีที่ว่าก็ต้อง "นึกถึงหัวอกคนเอาไปใช้ หรือนึกซะว่าถ้าคนใช้เป็นเราจะอยากได้อะไร" ก็คงเท่านั้น :)
 */
class FooPlugable extends Plugable{
     public $SampleMember = 'bla bla';
     function startFoo(){
        $SampleParam = 0;
        $this->ObserverNotify('onBeforStartFoo', array($SampleParam));
        /**
         * โดยส่วนมาก event ที่เป็น before คือเริ่มต้นทำอะไรซักอย่าง มักจะสนใจ result หรือ return ของ observer
         * เพื่อกำหนดว่าจะทำงานต่อหรือไม่ เราอาจจะเขียนเช็ค result ก่อนตรงนี้ และบอก ฝั่งผู้ใช้ว่าเราสนใจ result อะไร
         * เช่น ถ้าเราสนใจว่าหากมีการ return false มาจากฝั่ง observer เราจะไม่ทำงานต่อก็เขียนได้ ดังนี้
         *
         *     $results = $this->ObserverNotify('onBeforStartFoo', array($SampleParam));
         *     if(in_array(false,$results)) return; // โดยปกติค่า return นิยมกำหนดให้เป็น true/false (ถ้าจะให้มี)
         */
        // code
        $this->ObserverNotify('onStartFoo', array($SampleParam,'33'));
        // code
        $this->ObserverNotify('onAffterStartFoo', array($SampleParam, '55'));
        
        echo $this->SampleMember;
     }
}

/** 
 * ฝั่งผู้ใช้ หรือผู้เขียน plugin เราจะเขียน methods Notify ในนี้
 * ซึ่งจะเขียน method อื่นๆ ที่ไม่เกี่ยวกับ Notify ไว้ใช้งานภายในด้วยก็ได้
 */
class FooPlugin extends Plugin{
    /**
     * ถ้าต้องการ handle onBeforStartFoo
     *
     * @param  [Observable object] ผู้ทำการ Notify หรือ Sender
     * @param  [array] พารามิเตอร์ที่ส่งเข้ามา โดยมากนิยมเป็น array ซึ่งจะส่งอะไรมาบ้างผู้พัฒนาจะบอกไว้
     */
    function onBeforStartFoo($sender, $args){
        if($args[0]){
            $sender->SampleMember = 'my goo';
        }else{
            $sender->SampleMember = 'my girl';
        }
    }
    
    /**
     * ถ้าต้องการ handle จังหวะที่เกิด onStartFoo ก็ลักษณะเดียวกัน
     */
    function onStartFoo($sender, $args){
        // do something ....
    }
}

การโหลดเพื่อใช้งาน

/**
 * ตัวอย่างการทำแบบตรงไปตรงมา
 * แต่ในความเป็นจริงจะมีการสร้างระบบขึ้นมาบริหารจัดการต่างหาก อาจจะเรียกว่า PluginsManagement
 * เพื่อทำการโหลด observer และทำการเพิ่มเข้าไปใน Server
 */

$PlugAble = new FooPlugable();
$PlugAble->ObserverAdd(new FooPlugin());
/**
* now $PlugAble->SampleMember: 'bla bla'
**/

$PlugAble->startFoo();
/**
* now $PlugAble->SampleMember: 'my girl'
* startFoo output: my girl
/**

โค๊ดเพียวๆ Observer Pattern - PHP

interface Observer {
    function Notify();
}

interface Observable{
    function ObserverAdd(Observer $Observer);
    function ObserverRemove($ObserverKey);
    function ObserverNotify($NotifyName, $Args);
}

class Plugable implements Observable {

    private $_servers = array();

    function ObserverAdd(Observer $Observer){
        $k = get_class_name($Observer);
        $this->_servers[$key] = $Observer;
    }

    function ObserverRemove($ObserverName){
        if(isset($this->_servers[$ObserverName])
            unset($this->_servers[$ObserverName]);
    }

    function ObserverNotify($EventName, $Args = null){
        
        $results = array();
        
        foreach($this->_servers as $obj){
            if(in_array($EventName, get_class_methods($obj)){
                $results[] = $obj->$EventName(&$this, $Args);
            }
        }
        
        return $results;
    }
}

class Plugin implements Observer{
}

class FooPlugable extends Plugable{
     public $SampleMember = 'bla bla';

     function startFoo(){
        $SampleParam = 0;
        $this->ObserverNotify('onBeforStartFoo', array($SampleParam));
        /**
         *     $results = $this->ObserverNotify('onBeforStartFoo', array($SampleParam));
         *     if(in_array(false,$results)) return;
         */
        // code
        $this->ObserverNotify('onStartFoo', array($SampleParam,'33'));
        // code
        $this->ObserverNotify('onAffterStartFoo', array($SampleParam, '55'));
        
        echo $this->SampleMember;
     }
}

class FooPlugin extends Plugin{

    function onBeforStartFoo($sender, $args){
        if($args[0]){
            $sender->SampleMember = 'my goo';
        }else{
            $sender->SampleMember = 'my girl';
        }
    }
    
    function onStartFoo($sender, $args){
        // do something ....
    }
}

$PlugAble = new FooPlugable();
$PlugAble->ObserverAdd(new FooPlugin());
/**
* now $PlugAble->SampleMember: 'bla bla'
**/
$PlugAble->startFoo();
/**
* now $PlugAble->SampleMember: 'my girl'
* startFoo output: my girl
/**

อ้างอิง

http://en.wikipedia.org/wiki/Observer_pattern
http://www.ibm.com/developerworks/library/os-php-designptrns/
http://www.oodesign.com/observer-pattern.html
http://sourcemaking.com/design_patterns/observer

คราวหน้ามาลองทำ Observer + MVC


*** จำไว้ว่า: อย่ายึดติดกระบวนท่า ให้เข้าใจคอนเส็บ แล้วจะข้ามจากจอมยุทธ์ไปเป็นเซียน (อ. สุรเดช ม.นเรศวนฯ) ***


:) เอวัง