Wednesday, August 29, 2007

ทดลองเขียน Hangman บน Dojo 0.9

Dojo 0.9 ออกใหม่มาแล้ว ก็เลยต้องลองสักหน่อย
แน่นอน ก็ต้องเป็นเกมส์ Hangman
จะได้เอาไปโชว์ในงาน CNUG ด้วย

โดยผมจะลอง implment โดยแยก object ในเกมส์ เป็น component ย่อยๆ
เพื่อจะได้ทดลอง feeling ของการทำ modular ใน Dojo

จัดแบ่ง component ออกเป็น

+ cnug
+ hangman
- Game.js // domain object
- Graphics.js // ส่วนแสดงรูป
- Unreveal.js // ส่วนแสดงคำที่ให้ทาย
- Letters.js // ส่วนแสดงตัวอักษรให้กดเลือก
+ assets // เก็บภาพ
- ...png

ขั้นแรกสุด ลองมองที่ภาพรวมก่อน เวลาเอา module มาประกอบกัน,
หน้าตาของ file hangman.html เฉพาะส่วน body จะเป็นแบบนี้
<body>
<center>
<h1>Hangman</h1>
<div dojoType="cnug.hangman.Graphics"></div>
<div id="unreveal" class="letters" ></div>
<br/>
<div id="letters1" class="letters" dojoType="cnug.hangman.Letters" chars="abcdefghijklm" ></div>
<div id="letters2" class="letters" dojoType="cnug.hangman.Letters" chars="nopqrstuvwxyz" ></div>
<br/>
<div id="button" dojoType="dijit.form.Button" onclick="newGame()">New Game</div>
</center>
</body>

จะเห็นว่าเรานำ component มาวางต่อกันดังนี้
Graphics
Unreveal
Letters
Letters
Button

ใน component ที่นำมาวางข้างบน แยกเป็นสองพวกตามวิธี initialize widget,
ก็คือ พวกที่ initialize โดยวิธี declarative (โดยการระบุ attribute dojoType ลงไป)
กับพวกที่ initialize โดยวิธี programing

ลองดู javascript code ส่วนที่ทำหน้าที่ initialize โปรแกรม
// register module path
dojo.registerModulePath("cnug","../../cnug");

// require component
dojo.require("dojo.parser");
dojo.require("cnug.hangman.Game");
dojo.require("cnug.hangman.Unreveal");
dojo.require("cnug.hangman.Letters");
dojo.require("cnug.hangman.Graphics");
dojo.require("dijit.form.Button");

// initialize
dojo.addOnLoad(function() {
game = new cnug.hangman.Game();
var unreveal = new cnug.hangman.Unreveal({game: game}, dojo.byId("unreveal"));
dojo.connect(dijit.byId("letters1"), "select", game, "guess");
dojo.connect(dijit.byId("letters2"), "select", game, "guess");
newGame();
});

การ registerModulePath จะช่วยให้เราสามารถจัดแยก code ที่เราเขียน
กับ code ที่ dojo provide มาให้ เพื่อให้สะดวกต่อการ maintain version control ของโปรเจค
ขั้นตอนการทำงานหลักๆ ก็คือ
  • new Game object
  • สร้าง unreveal Component โดยผ่านค่า game instance เข้าไปทาง constructor ของ Unreveal
  • เชื่อม event ระหว่าง Letters component เข้ากับ Game instance
    เพื่อที่เวลา user กดเลือกตัวอักษร, game instance จะได้ update state ตัวเอง
  • เรียก function newGame ที่จะทำหน้าที่ ajax call ไปขอคำศัพท์ จาก server
    และ set คำศัพท์นั้นให้กับ Game object


ลองมองเข้าไปใน Component แต่ละตัวบ้าง
เริ่มที่ Component Game ก่อน
หน้าตาของ Game.js เป็นแบบนี้
dojo.provide("cnug.hangman.Game");

dojo.declare("cnug.hangman.Game", null, {

newGame: function(word) {
this.word = word
this.unreveal = word.replace(/./g, '_')
this.mistake = 0;
this._fireNewGame();
},

guess: function(ch) {
var idx = this.word.indexOf(ch);
if (idx >= 0) {
var tmp = '';
for (var i = 0; i < this.word.length; i++) {
if (this.word.charAt(i) == ch) {
tmp += ch;
} else {
tmp += this.unreveal.charAt(i);
}
}
this.unreveal = tmp;
} else {
this.mistake++;
}
this._fireChange();
},

isWin: function() {
return this.unreveal.indexOf('_') < 0;
},

isLose: function() {
return this.mistake > 5;
},

_fireChange: function(){
dojo.publish("stateChange", [this]);
},

_fireNewGame: function(){
dojo.publish("newGame", [this]);
}
});

ประเด็นที่น่าสนใจก็มี
  • dojo.provide ก็คือคำสั่งที่ระบุว่า file นี้เป็น module อะไร
  • dojo.declare ก็คือ คำสั่งที่ใช้สร้าง Class Person (OOP ใน javascript เป็นพวก prototype base)
  • จะเห็นว่า method จะไม่มี Visiblilty scope กำกับ
    ทุกอย่างเป็น public หมด
    เราก็เลยใช้วิธีตั้งชื่อ อันไหนที่เป็น internal ก็นำหน้าด้วย underscore เสีย
  • การสื่อสารระหว่าง Game Object กับ component อื่นๆ
    ใช้ feature Publish-Subscribe ของ Dojo


ลองดู component ที่เป็น UI Widget บ้าง
เริ่มที่ Letters.js ที่ทำหน้าที่สร้างแถวตัวอักษรที่ user สามารถเลือก click ได้
และเมื่อ click แล้ว ตัวอักษรนั้น ก็จะ disabled ไม่ให้เลือกซ้ำอีก

ลองดูตัวเล็กสุดก่อน นั่นก็คือ LetterLink ที่ represent ตัวอักษรแต่ละตัว
dojo.declare("cnug.hangman.LetterLink", dijit.form._FormWidget,
{
char: '',

parent: null,

templateString: "<span><a href='#' dojoAttachPoint='anchor'
dojoAttachEvent='onclick: _select'>${char}</a> </span>"
,

_select: function() {
if (! this.disabled) {
this.setDisabled(true);
dojo.addClass(this.anchor, "disabledLink");
this.parent.select(this.char);
}
},

_reset: function() {
this.setDisabled(false);
dojo.removeClass(this.anchor, "disabledLink");
}
});

ประเด็นที่น่าสนใจ
  • วิธีการ declare Object ที่ extends จาก Object อื่นๆ
    เราสามารถใช้คำสั่ง dojo.delare("myclass", subclass, { // class body });
    หรือถ้า inherrit มาจากหลาย object ก็ใช้ dojo.delare("myclass", [subclass1,subclass2], { // class body });
    (ใช้คำว่า class เพื่อให้คนคุ้นกับ java เข้าใจ, แต่จริงๆมันคือ object หรือ function)
  • เราใช้ Object "dijit.form._FormWidget" เป็นต้นแบบ
    เนื่องจากมันมี คุณสมบัติหลายอย่างที่เราสามารถนำมาใช้ได้เลย เช่น disable, enable
  • templateString ก็คือการกำหนดว่าหน้าตาของ html ที่เรา generate ออกไปจะเป็นอย่างไร
    ใน dojo, เราสามารถแทรก attachPoint, attachEvent ใน template ได้
    อย่างใน code ข้างบน เรามี dojoAttachPoint เป็นค่า "anchor"
    หมายความว่า Dojo จะ inject DOM นั้นลงใน property ที่ชื่อ "anchor" ให้เราโดยอัตโนมัติ
    ทำให้เราสามารถเขียน this.anchor.src = 'xx' ได้เลย
    ส่วน dojoAttachEvent ทำให้เราร้อย event จาก DOM
    เข้ากับ function ใน Object เราได้ง่ายๆ

ย้อนกลับมาดู Letters ที่ทำหน้าที่เป็น container ของ LetterLink บ้าง
dojo.declare("cnug.hangman.Letters", [dijit._Widget, dijit._Container],
{
chars: '',

charsWidget: null,

postMixInProperties: function() {
this.charsWidget = [];
},

postCreate: function() {
var cs = this.chars.split('');
for (var i=0; i < cs.length; i++) {
this.charsWidget[i] = new cnug.hangman.LetterLink({char: cs[i], parent: this});
this.addChild(this.charsWidget[i]);
}
dojo.subscribe("newGame", this, "reset");
},

reset: function() {
dojo.forEach(this.charsWidget, function(elm) {
if (elm.disabled) {
console.debug("reset" + elm);
elm._reset();
}
});
},

select: function(ch) { console.debug(ch);}
});

ประเด็นน่าสนใจคือ
  • object นี้ inherrit จาก dijit._Widget และ dijit_Container
    คือเป็นทั้ง Widget ที่แสดงบนหน้าจอ และสามารถ container Widget ตัวอื่นๆได้
  • method ของ dijit._Widget ที่เรา override ก็คือ
    postMixinProperties ที่จะถูกเรียกหลังจากที่มีเสร็จสิ้นการ set property ต่างๆให้ object
    ส่วน postCreate ถูกเรียกใช้ เมื่อสิ้นสุดขบวนการสร้าง Widget แล้ว
  • select function เป็น function เปล่าๆ
    ใช้เป็นจุด extension point ให้สามารถร้อย function นี้
    เข้ากับ function ของ Object อื่นๆ ตามที่เราต้องการ
    (จะใช้กลไก publish-subscribe ก็ได้ แต่เบื่อแล้ว ก็เลยลองอย่างอื่นบ้าง)
  • พวก chars, charsWidget เป็น property ของ Object Letters
    ประเด็นที่ต้องระวังก็คือ พวก property ที่ชี้ไปยัง object (พวกที่เป็น by reference)
    จะต้อง initialize ใน postMixinProperties มิเช่นนั้น มันจะกลายเป็น
    static reference ที่ share กันระหว่าง object instance ของ Letters
    ลองดูตัวอย่างจาก code ง่ายๆนี้
    dojo.declare("Person",null, {
    item: [] // กลายเป็น static reference
    }
    p1 = new Person();
    p2 = new Person();
    p1.item[0] = 1;
    p2.item[1] = 2;
    alert(p1.item); // => [1,2]



ชักจะยาวไปแล้ว
ใครสนใจดูรายละเอียด ไปคุยกันได้ที่งาน CNUG ครับ

Note: ตอนแรกว่าจะลองเขียนด้วย YUI ด้วย
แต่พบว่า YUI เขียนเป็น modular แล้วไม่สนุกเท่า Dojo
ก็เลยเลิก

Related link from Roti

3 comments:

Prach Pongpanich said...

สบายดีไหมพี่ป๊อก ไม่ได้เจอกันนานเลย งาน BTD 3 ผมก็ไม่ได้ไปเสียดาย ๆๆๆ

polawat phetra said...

หายเงียบไปเลย
ช่วงนี้ทำอะไรอยู่
เขียน mail มาเล่าใ้ห้ฟังบ้าง

(mail พี่ ก็ pphetra AT gmail)

Anonymous said...

ไม่ทราบว่ามีสาวๆ หรือเปล่าครับ อยู่ใกล้บริษัทแค่นี้เอง อ่ะ

จะไปร่วมแจมด้วยจะได้หรือเปล่าครับ

ปู่