-
Notifications
You must be signed in to change notification settings - Fork 0
Observer Pattern
Observer Pattern เป็นหนึ่งใน pattern ที่ใช้กันมากบน Framework ต่างๆ (ไม่ว่าจะภาษาใดก็ตาม) เนื่องจากเป็น Pattern ที่ตอบโจยท์เรื่อง การแก้ไข/เพิ่มเติมความสามารถของ Core Framework โดยไม่ต้องเข้าไปแก้ไขใน Core จริงๆ หรือที่เรียกว่า Hacking
แม้ว่า Framework หลายๆ ตัว โดยเฉพาะ PHP Framework จะเป็น Open source ก็ตาม การที่เราจะแก้ไขหรือเข้าไปเพิ่มเติมความสามารถบางอย่างใน code โปรแกรมโดยตรงนั้น ถือว่าไม่ใช่วิธีที่ถูกต้อง เพราะแม้จะเป็น Open source เราก็ยังต้องมอง Framework ต่างๆ ในลักษณะของ software แบบ Black-box (กล่องดำ) ที่ไม่สามารถ (ไม่ควร) เข้าไปแก้ไขอะไรเลยอยู่ดี เนื่องจากว่าหากฝั่งผู้พัฒนา 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 อีกด้วย
ประกอบไปด้วย 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
/* ลักษณะ 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
/**
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
*** จำไว้ว่า: อย่ายึดติดกระบวนท่า ให้เข้าใจคอนเส็บ แล้วจะข้ามจากจอมยุทธ์ไปเป็นเซียน (อ. สุรเดช ม.นเรศวนฯ) ***
:) เอวัง