[修練之路-JS]3. JS’s Observer pattern (using prototype)

[修練之路-JS]3. JS’s Observer pattern (using prototype)

參考文件:

Essential JavaScript And jQuery Design Patterns – A Free New Book,

observer pattern concept:http://www.dofactory.com/Patterns/PatternObserver.aspx#_self2

super.func():http://phrogz.net/js/classes/OOPinJS2.html

prototype:http://msdn.microsoft.com/zh-tw/scriptjunkie/gg476048.aspx

 

觀察者樣式是重要性/實用性很高的樣式, 本系列文件會分3篇來demo

第一篇. 使用prototype實現物件導向設計下的觀察者樣式.

第二篇. 使用closure實現JavaScript強大語法下的觀察者樣式.

第三篇. 使用jQuery實現方便我們實作的觀察者樣式.

 

觀察者樣式簡單的舉個例子, 像是我們平常在寫畫面程式時, 增加方法去觀察使用者的按鍵/滑鼠行為後做處理, 即為一種常見的用法;

 

在觀察者樣式中, 有兩個重要的Type:

1. 主題(Subject)

2. 觀察者(Observer)

 

三個重要的Operator

1. 訂閱(Subscribe)

2. 取消訂閱(Unsubscribe)

3. 通知更新(Notify/Publish/Update …)

 

以類別圖來解說此回的案例,

主題為位置(Location) 即為 x , y ,

而觀察者(CodeGenerator)則是要依 x , y 產生出代碼(Code),

另外增加了參數 z 來豐富產生代碼的觀察者(TimeCodeGenerator/RandomCodeGenerator),

在畫面上的主程式則是由LocationTracker來處理滑鼠移動時的x , y值.

Class Diagram0

在循序圖我們可以簡要的看到樣式的經過

1. 畫面的主程式建立位置(Location)物件

2-5 觀察者(CodeGenerator)訂閱位置.

6. 當位置變更時通知所有的觀察者建立其代碼

7. 取消訂閱

Sequence Diagram0

在案例中, 我們要做的如下圖, 當啟動時, 在空白處滑動來產生2種不同的代碼, 按下Start It 來啟動, 或按下End It結束.

image

 

因為有用到jshashtable.js 故記得要先去下載, 詳細的解說都在程式的註解中, 可以直接RUN 的版本在 http://jsfiddle.net/hectorlee369/Vmcjs/

 

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<script type="text/javascript" src="jshashtable.js"></script>
<script type="text/javascript">
// begin Subject ======== 
// 主題界面, 訂義訂閱(subscribe), 取消訂閱(unsubscribe), 發佈(notify/publish)
var Subject = function(){ this.initialize(); }
    
    Subject.prototype.initialize=function(){
        return null;
    }    
    Subject.prototype.subscribe=function(observer){        
        return null;
    }
    Subject.prototype.unsubscribe=function(id){
        return null;
    }
    Subject.prototype.notify=function(args){
        return null;
    }
// end Subject ========    
 
// begin Location ========
// 位置界面, 實作訂閱(subscribe), 取消訂閱(unsubscribe), 發佈(notify/publish)
var Location = function(){ this.initialize(); }
    // 繼承
    Location.prototype = new Subject();
    // implement 以Hashtable 當作訂閱的儲存集合
    Location.prototype.initialize=function(){
        this.observers = new Hashtable();
        this.current_id = 0; // 遞增給每一個訂閱者當做id
    }
    // implement
    Location.prototype.unsubscribe=function(id){
        this.observers.remove(id);
    }
    // implement 加入到訂閱集合中
    Location.prototype.subscribe=function(observer){        
        this.current_id++ ;
        this.observers.put( this.current_id, observer );
        
        return this.current_id;
    }
    // implement 通知每一個訂閱者
    Subject.prototype.notify=function(x, y){
        this.observers.each(function(key, value){ 
            value.generate({ x : x, y : y }); 
        });
    }
// end Location ========
 
 
// begin Observer ========
// 觀察者界面, 訂義製作(generate)
var Observer = function() { this.initialize(); }
    
    Observer.prototype.initialize=function(){
        return null;
    } 
 
    Observer.prototype.generate=function(args){        
        return null;
    }
// end Observer ========    
 
// begin CodeGenerator ========
// 觀察者-代碼建立, 觀察Location(x, y)值後建立代碼
var CodeGenerator = function() { this.initialize(); }
    // 繼承
    CodeGenerator.prototype = new Observer(); 
    // override, 增加類別屬性: code
    CodeGenerator.prototype.initialize=function(){ 
        this.code = "";    
    }
    // override , 增加參數: [args[x, y,max_length, get_z, ctrl_id]]
    // max_length: <int> 代碼長度
    // get_attr_num: <function> 子類別提供代碼產生之數值參數
    // ctrl_id: html dom object id
    // 依參數 (x, y , get_z) 運算產生一個A-Z的字元, 並顯示於畫面中.
    CodeGenerator.prototype.generate=function(args){    
        if(this.code.length< args.max_length + 1 ){            
            var c = String.fromCharCode(65 + (args.x + args.y + args.get_z()) % 26);
            this.code+=c;        
            document.getElementById(args.ctrl_id).innerText = this.code + "  [xxx 未完成 xxx]";
        }else{
            document.getElementById(args.ctrl_id).innerText = this.code + "  [ooo 已完成 ooo]";
        }        
    }    
// end CodeGenerator ========
 
// begin TimeCodeGenerator ========
// 觀察者-依時間為參數產生代碼
var TimeCodeGenerator = function() { this.initialize(); }
    // 繼承
    TimeCodeGenerator.prototype  = new CodeGenerator();
    // override
    TimeCodeGenerator.prototype.generate=function(args){
        var get_z=function(){
            return new Date().getTime();
        }
        // 呼叫父類別方法, 類似super.xxx() or base.xxx()
        CodeGenerator.prototype.generate.call(this, { x:args.x, y:args.y, ctrl_id : 'time_gener', max_length : 32, get_z: get_z });
    }
// end TimeCodeGenerator ========
 
// begin RandomCodeGenerator ========
// 觀察者-依亂數為參數產生代碼
var RandomCodeGenerator = function() { this.initialize(); }
    // 繼承
    RandomCodeGenerator.prototype = new CodeGenerator();
    // override
    RandomCodeGenerator.prototype.generate=function(args){
        var get_z=function(){
            return Math.random() * 100000 ;
        }
        // 呼叫父類別方法, 類似super.xxx() or base.xxx()
        CodeGenerator.prototype.generate.call(this, { x: args.x, y:args.y, ctrl_id : 'random_gener', max_length : 16, get_z: get_z });        
    }
// end RandomCodeGenerator ========
 
 
// begin LocationTracker
// 處理畫面功能的啟動/關閉
var LocationTracker = function(){ this.initialize(); }
    LocationTracker.prototype.initialize=function(){
        this.subject = new Location();
        this.subjectId_01;
        this.subjectId_02;
        
        this.status=0;
    }
    
    LocationTracker.prototype.start=function(){        
        this.end();
        this.subjectId_01 = this.subject.subscribe( new TimeCodeGenerator() ) ;
        this.subjectId_02 = this.subject.subscribe( new RandomCodeGenerator() ) ;        
        this.status=1;
        document.getElementById('holder').style.display='block';
    }
    LocationTracker.prototype.end=function(){        
        if(this.subjectId_01){
            this.subject.unsubscribe(this.subjectId_01);
            this.subjectId_01=null;
            document.getElementById("time_gener").innerText = "";
        }
        if(this.subjectId_02){
            this.subject.unsubscribe(this.subjectId_02);
            this.subjectId_02=null;
            document.getElementById("random_gener").innerText = "";
        }
        this.status=0;
        document.getElementById('holder').style.display='none';
    }
    
 
var tracker = new LocationTracker();
 
</script>
</head>
<body onload="tracker.start()">
<input type="button" value="Start It" onclick="tracker.start();" /> &nbsp;&nbsp;
<input type="button" value="End It" onclick="tracker.end();" /> <p />
<div id="holder">
    請在空白處滑動你的mouse來產生代碼:
    <div style="height:300px; width:300px; border-width:1px; border-style:solid" onmousemove="if(tracker.status==1) tracker.subject.notify(event.x, event.y)"></div>
    <p>TimeCodeGenerator:   <span id="time_gener" ></span> </p>
    <p>RandomCodeGenerator: <span id="random_gener" ></span> </p>
</div>
</body>
</html>    

 

印象中不知道在那一份JavaScript牛人的文件中有寫到, 這種寫法就是寫JAVA 或 C#物件導向語言走火入魔的方式, 即不美觀又笨重肥大也不... , 總之被嫌到爛, 可是以我有限的腦神經, 其實最看得懂這種Coding style, 所以把它放在第一篇. 下回就要挑戰即美觀又輕巧的closure ~~