顯示具有 教學 標籤的文章。 顯示所有文章
顯示具有 教學 標籤的文章。 顯示所有文章

2014-06-01

[新手止難] 為什麼有些東西要弄得那麼複雜?

昨日驚見一文〈「互相之臉」所謂何用〉 [註1],
討論文章數目,為數月來爪哇連線版鮮見現象。
拜讀過各家之言後,回顧提問者之疑……

此時夜半人靜、身困馬桶不得其門而出
沉吟許久,上下交迫間,腦中浮現劣思緒緒
顧不得少年得痔之慮,返回書桌振鍵急敲,以求一吐為快
乃成於此篇陋文,敗筆處處
還望各方高人雅士撥冗指點一二,以正方家


敝痞在資訊領域打混唬爛多年
整理出〈資訊三大唬爛金句〉
其中首要綱領,為挪用錢鍾書所言:

道不虛明,有為而發  

或用俗語釋之

殺頭生意有人作  
賠本生意沒人幹  

雖凡天下學問,約莫均可套用此理
但惟資訊領域,可全然貫穿通透

只因此門學問均屬人為打造,鮮少摻雜自然現象
間或參雜不確定性,也可用機率模型解釋
又因效率為首,一切循規蹈矩最為便利
是謂「資訊領域少例外,規格讀通各處行」

所以,當吾人遇一理論或一設計,艱澀難懂
正當唸之無味棄之被當,國罵聲不絕與耳時
不妨憶此金句,自問之:

懶惰乃人類之美德  
若發明者不夠懶惰,安可流傳至今?

抱此想法續讀,不僅情緒平復、易於心領神會
倘使發現更懶惰之法,圖林獎豈非垂手可得?


今吾人已瞭解其所敘述之架構
但僅見「超頭」處處 [註2]
再三盤算依然覺得:此乃蝕本生意,作不得! 作不得!
莫非敝痞之虎爛金句有誤? 非也…

在此,逆舉老子之說,搭配程式設計為例:

小國寡民,使有什佰之器而不用,使民重死而不遠徒。  
雖有舟輿,無所乘之;雖有甲兵,無所陳之;使民復結繩而用之。  
甘其食,美其服,安其居,樂其俗。  
鄰國相望,雞犬之聲相聞,民至老死不相往來。

將「國」類比至「程式規模」
將「民」類比至「程式設計師」
將「舟輿」、「甲兵」類比至各式軟體架構

若是程式規模小彼此之間關係簡單
又或是無須與其他程式設計師往來
吾人可任意而為之,無所窒礙
此時亦難窺得舟輿甲兵之利,也就無從了解軟體架構之妙

禪宗分南北,北宗主漸修,南宗主頓悟
若漸修之途不可得,何妨留至來日機到緣成,自然頓悟?

敝痞此說,並非低漸修而貴頓悟
漸修之途不可省,則來日方能有頓悟之緣
即便汝輩天縱英名,亦不能自恃而於此處偷懶
以免淪於「重新再造輪」之窘境,慎之! 慎之!

[註1] 「互相之臉」亦有人美譯為「臉間」
其原文為 interface,常見地球語稱之「介面」 \囧/

[註2] 「超頭」其原文為 overhead \囧/


原文寫於 2006.04.15 PTT Java 版。

嗯… 那個時候國文比較好 [笑]

[新手止難] 寫程式就像寫作文 \囧/

各位 Java 版的版友大家好, 我的唬濫分析師痞子猴

痞子今天來這裡幹什麼?
教大家怎麼唬濫新手嘛…

痞子有沒有說,寫程式就像寫作文
你一定要把題目搞清楚
不然馮京變馬涼,就算有痞子那樣唬濫的功力
寫出來的程式還是錯嘛
神 仙 難 救 白 目 客

……..

國文老師有沒有說
寫作文要有起承轉合、要立論嚴謹、要思路清晰
痞子有沒有說,程式要定義好 Input 跟 Output
整個流程都能用火星文講給任何一個火星人聽得懂
你有聽痞子的話嗎? 沒有嘛…
自己都還沒想清楚整個 Input,Output 跟流程
就跑到板上問要怎麼寫、問要用什麼語法、問要用什麼類別
結果大家都以為你是作業文,把你這篇文章作空
你要痞子怎麼辦? 痞子也不知道怎麼辦… [丟滑鼠]

……..

國文老師有沒有說,字跡要端正、成語要用對、要避免錯別字
痞子有沒有說,縮排排版要確實、基礎語法要搞清楚、要避免不良變數名稱
你有聽痞子的話嗎? 沒有嘛…
結果程式碼迴圈區段搞錯,語法 & 邏輯錯誤一堆,程式碼難讀難懂
搞得作多也不是,作空也不成
你要痞子怎麼辦? 痞子也不知道要怎麼辦… [丟鼠墊]

……..

國文老師有沒有說,立論的問題點要各個擊破
痞子有沒有說,要把功能切細再切細,然後一小個一小個分開解決
你有聽痞子的話嗎? 沒有嘛…
重頭到尾都混亂不清 bug 一堆,結果大盤走勢不明朗
你要痞子怎麼辦? 痞子也不知道該怎麼辦… [丟鍵盤]

……..

好的痞子教你用 Java,壞的痞子要你用 .net
結果你不好好學 Java,現在要寫案子了,才發現被 M$ 套牢
你要痞子怎麼辦? 痞子也不知道怎麼辦… [丟螞蟻書]

……..

好! 我們進一段廣告
為了慶祝痞子順利 po 完這篇文章
廣告期間徵召各類 Java 駐版技術達人
有優待!

進廣告


原文寫於 2006.02.23 PTT Java 版,語氣是模仿當時一位股票名嘴的風格。

2011-09-28

用 Channel API 實作簡易聊天室

Google App Engine 自 1.4.0 版推出 Channel API,使 server 與 browser 之間可以不透過 pooling 的方式做到 server push。不過 Google 官方教學文件摻雜了井字遊戲的元素,反而無法專注於 Channel API 上。這篇文章打算用最原始的聊天室,透過實做的過程來體驗一下 Java 版 Channel API。

因為是簡易聊天室,所以只打算提供一個共用的聊天室,然後用兩個 JSP 檔來解決:room.jsp、server.jsp。room.jsp  負責處理使用者輸入訊息、顯示對話;server.jsp(應該寫成 servlet 比較好,因為是簡易聊天室......)負責接收訊息、並廣播出去。

對應到 Channel API 的用詞,一個聊天室就是一個 channel,同一個 channel 的成員(client 端)就會接收到其他成員所發出的訊息(message)。所以,從 server 的角度,需要作這些事情:
  1. 開啟一個 channel
  2. 給加入這個 channel 的 client 專屬識別碼(token)
  3. 接收訊息的管道
  4. 發送訊息的功能
在 Channel API 當中只要決定 channel 的 名字就算做到第一點了,因為是簡易聊天室,所以把名稱訂死為「PsMonkey」。產生 token 的方法是:
//Java code
ChannelService channel = ChannelServiceFactory.getChannelService();
String token = channel.createChannel("PsMonkey");

因為是簡易聊天室,所以把 token 透過 JSP 產生、並塞進 client 端的 JavaScript 碼。

接下來看 client 端的部份。首先建立連線的部份:
//JavaScript code
var channel = new goog.appengine.Channel("<%=token%>");  //token 傳入
var handler = {
	'onopen' : onOpened,  //建立連線、channel.open() 就會觸發
	'onmessage' : onMessage,  //有訊息傳入時
	'onerror' : onError,  //發生錯誤時
	'onclose' : onClosed,  //連線結束時
};
socket = channel.open(handler);

socket 就是實際處理通訊的部份。這段的重點是透過 handler 設訂 socket 各種狀況會觸發的 method。仔細想一下就會發現:「只有接收訊息的 method,那傳送訊息呢?」

Channel API 要解決的問題是 server push,client 端傳送訊息的功能並不包含在其中,得透過 HTTP request 來作到。所以送訊息的 method 會長的像這樣:
//JavaScript code
sendMessage = function(message) {
	var xhr = new XMLHttpRequest();
	xhr.open('POST', 'server.jsp?msg='+message, true);
	xhr.send();
};

於是用 sendMessage() 來設定在 client 端連線時送出「____ 進入聊天室」、離線時送出「____ 離開聊天室」:
//JavaScript code
onOpened = function() {
	sendMessage("「"+name + "」進入聊天室......");
};

onClosed = function(){
	sendMessage("「"+name + "」離開聊天室......");
}

有訊息進來的時候,就把訊息塞進某一個 element 的尾巴。這邊要注意一點,參數 msg 不是單純的字串,而是一個物件,data 這個 field 才是真正回傳的資料。
//JavaScript code
onMessage = function(msg) {
	document.getElementById("output").innerHTML += msg.data +"<br />";
}

接著來看一下負責處理 client 端 sendMessage() 的 server.jsp,直接貼程式碼:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@page import="com.google.appengine.api.channel.*"%>
<%
String message = request.getParameter("msg");
ChannelService channel = ChannelServiceFactory.getChannelService();
channel.sendMessage(
	new ChannelMessage("PsMonkey", message)
);
%>

在從 request 當中取得 clinet 端傳上來的訊息後,連同 channel 的名稱包成一個  ChannelMessage,透過 ChannelService.sendMessage() 傳出去,就會觸發 client 端的 socket.onMessage 了。

再透過一些簡單的 HTML 與 JavaScript 來讓 client 端送出訊息時能呼叫到 sendMessage(),聊天室就完成了!是不是很簡單呢? [扭扭]

最後補充 socket 的幾件事情:

  • onerror 會傳入一個參數,其中有兩個 field:
    • description:錯誤的描述
    • code:HTTP 錯誤代碼
    • 目前只測出 idle 太久、server 切斷連線時會觸發
  • socket 還有一個 method:close(),呼叫成功會觸發 socket.onclose。
完整可以跑的範例放在這裡...

2011-09-01

粗探 GWT Image 內的實做方式

GWT 當中的 Widget,大概只有 Button 的使用率大於 Image(這也難講,說不定有些人直接用 Image 作 button...... XD)。所以來探究一下 source code 寫了啥東西。

要建立一個 Image,最直接了當的用法是給它 url:
Image img = new Image("http://an.url/pic.jpg");

這裡的 url 使用相對路徑亦可,不過得注意是相對於載入這個 GWT module 的頁面就是了。

切到這個 Image(String) 這個 constructor,會發現當中做了兩件事情:
  1. 將 state 設定為新產生的 UnclippedState 物件
  2. 設定 style name 為 "gwt-Image"

設定 style name 這檔子事情有點無關緊要,忽略不管。至於 state 這個 field 是怎麼一回事?這似乎得要回頭看開頭 JavaDoc 寫的:
「The image can be in 'unclipped' mode (the default) or 'clipped' mode.」
也就是說,Image 透過 state 來決定當下是哪一種使用方式,這可以解釋為甚麼有另一個 Image(String, int, int, int, int) 的 constructor。實際去看 Image 的 method 行為,getter、setter、onLoad() 等都是由 state 負責,只有跟 event handler 有關的 method 是用 Widget.addHandler() 處理。

於是找到 Image 裡頭 State 這個 private 的 abstract class。它只有兩個 method 不是 abstract 的:onLoad(Image)、fireSyntheticLoadEvent(Image),這兩個都跟 event 有關,在這篇文章當中先略過,以免枝節太多。其餘的 abstract method 留給 ClippedState、UnclippedState 實做。

比對 ClippedState 與 UnclippedState 實做 method 的差異,差別在於 ClippedState 多了 width、height、left、top 這幾個 field,UnclippedState 在取這些值時,是直接對 Image 所屬的 ImageElement 取值,而 ClippedState 則直接回傳 field 的值。另外,如果叫用到 ClippedState.setUrl() 會將 state 切換到 UnclippedState;反之,如果叫用到 UnclippedState.setVisibleRect() 與 setUrlAndVisibleRect() 會將 state 切換到 ClippedState。

這到底在幹什麼?

回歸實際用途,ClippedState 的目的是只顯示圖片上的某個矩形區塊,所以得加上長、寬、起始位置等資訊。製造出這種效果的方法,則是靠 CSS 設定 span 的 background 來辦到。怎麼知道的呢?答案在 constructor 當中的這行:
image.replaceElement(impl.createStructure(url, left, top, width, height));

先用 ClippedImageImpl.createStructure() 製造出一個 Element 物件,然後再把原本 Image 的 element 換成這個。再仔細看一下 ClippedImageImpl 裡頭的寫法,發現它直接用最硬幹的方式設定 element 的 HTML 與 style,也難怪會抽出去自成一個 class。這說明了為甚麼 JavaDoc 強調當 clipped 與 unclipped 互換時,所有的 style 設定都會消失。

重新回到 Image,會發現這個 class 只是邏輯上的存在,為了做到 clipped/unclipped 的效果,所以透過 State 來處理;因為底層實做方式的不同,所以當物件的行為模式轉換時變更到對應的 state。而真正對應 DOM,則是 ImageElement(unclipped mode)與 SpanElement(clipped mode)。使用 Image 時哪會想到底下這麼多怪東西?好的 class 如當是也......

Image 最後還有一個 perfetch(String) 的功能,就留待下回分解了(?)


這篇是退伍後的復健治療... 所以... [遮臉]

2010-08-26

對戰吧~GWT 踩地雷! [下]

跟電腦(web server)對戰的故事大概是這樣的:
  1. 跟 server 要求開始一場遊戲
  2. 跟 server 取得遊戲資訊
  3. 根據遊戲資訊繪製畫面
  4. 將玩家踩的地點傳送給 server
    1. 命中地雷→更新遊戲資訊
    2. 沒有命中→輪到 AI 踩地雷→更新遊戲資訊
  5. 檢查是否有某方獲勝?
    1. 有→結束。
    2. 沒有→回到步驟 3
粗體的部份是跟 server 有關的部份。
那麼,以 RPC、或說以程式的講法
server 只需要提供兩個 method
  • startGame():負責開遊戲 
    • 回傳:該場 game 的 uid
  • shoot():傳送玩家踩的地點
    • 參數:uid, x, y
    • 回傳:GameInfo 物件
因為懶得多設計一堆有的沒的
所以開場之後讓 client 端程式自動踩 (-1,-1) 這個非法位置
就會進入到步驟 2~4 的循環當中 [逃]

好了,終於要進入到 GWT RPC 的部份了。
這裡打算跳過理論、架構的部份
直接以程式碼來說明一切...

首先,是要有一個 MineService 的 interface
繼承自 com.google.gwt.user.client.rpc.RemoteService
裡頭就是宣告上頭說得那兩個 method
@RemoteServiceRelativePath("mineRPC")
public interface MineService extends RemoteService {
  public String startGame();
  public GameInfo shoot(String id, int x, int y) throws Exception;
}
至於那個 annotation 先跳過,後頭會解釋

再來是一個對應的 interface,名稱通常是後頭補 Async
public interface MineServiceAsync {
  void startGame(AsyncCallback<String> callback);
  void shoot(String id, int x, int y, AsyncCallback<GameInfo> callback);
}

這兩個 class 必須放在 gwt compiler 會處理的目錄下(例如 client)

有 interface 自然有實做的 class
MineService 對應實做 class 通常叫做 MineServiceImpl
會長成這樣子:
public class MineServiceImpl extends RemoteServiceServlet implements MineService {
  private AI_Interface ai = new RandomAI();  //FIXME change your ai here!
  
  @Override
  public String startGame() {
    String id = UUID.randomUUID().toString();
    MineGM setting = new MineGM();
    setServer(id, setting);
    return id+",Random";
  }
    
  private void setServer(String id, MineGM setting){
    this.getThreadLocalRequest().getSession().setAttribute(id+"ID", setting);
  }
  
  private MineGM getServer(String id) throws Exception{
    String name = id+"ID";
    if(this.getThreadLocalRequest().getSession().getAttribute(name)!=null){
      return (MineGM) this.getThreadLocalRequest().getSession().getAttribute(name);
    }else{
      throw new Exception("還沒開局");
    }
  }
  
  @Override
  public GameInfo shoot(String id, int x, int y) throws Exception{
    MineGM server = getServer(id);
    if(x==-1 || y==-1 || server.getMap()[x][y]!=-1){
      return MineGM.toGameInfo(server);
    }
    
    if(!server.shoot(x, y, MineGM.USER)){
      int[] xy = new int[2];
      do{
        ai.guess(MineGM.toGameInfo(server), xy);
      }while(server.shoot(xy[0], xy[1], MineGM.AI));
    }
    
    setServer(id, server);
    return MineGM.toGameInfo(server);
  }
}

這個 class 還會繼承 RemoteServiceServlet
往上追溯,parent 是 HttpServlet
也就是說,MineServiceImpl 也是一個 HttpServlet
雖然 GWT RPC 很神奇地包裝好許多東西
但終究還是 base on JSP
所以寫這個 class 時,就不用管 GWT 的重重限制
只要 web.xml 有設定正確就好

講到 web.xml,回頭講一下 MineService 的 annotation
用 RemoteServiceRelativePath 設定 servlet-mapping 會比較方便
<!-- in web.xml -->
<servlet-mapping>
 <servlet-name>mineRPC</servlet-name>
 <url-pattern>/_mine/mineRPC</url-pattern>
</servlet-mapping>

url-pattern 的值,前半段 _mine 是在 gwt.xml 中
設定 <module rename-to='_mine' >
後半段 mineRPC 就是在 MineService 設定的值
(沒有用這個 annotation,得要多好幾行煩死人的 code)

至於遊戲資訊,我選擇塞在 session 當中
在 RemoteServiceServlet 要取得 session 比較囉唆一點
得要這樣才能取得 session
this.getThreadLocalRequest().getSession()
其餘的程式碼... 嗯... 不在 GWT 的範圍當中,跳過 XD

server 端的程式碼解決了,現在來看 client 端
client 端必須透過 MineServiceAsync 來呼叫 RPC
不過用法有點奇怪...... Orz

首先要先這樣寫,取得一個 MineServiceAsync 的 object
MineServiceAsync msa = GWT.create(MineService.class);
然後就可以用 msa.shoot() 來告訴 server 要踩哪個位置
但是事情還沒完,除了標準的 parameter
得要傳一個為 AsyncCallback 的 parameter
這是讓 server 端處理完畢後可以 callback 的一個手段
初期通常都會用 anonymous class 來解決
所以程式碼會長得像這樣:
msa.shoot(this.gameID, hitX, hitY, new AsyncCallback<GameInfo>(){
      @Override
      public void onFailure(Throwable caught) {
        Window.alert("shoot : "+caught.getLocalizedMessage());
      }

      @Override
      public void onSuccess(GameInfo result) {
        setShootResult(result);
      }
    });

這邊要注意兩件事情

其一,RPC 傳遞/回傳的 class(及其 field)
除了 primitive data type 外
一定得 implements IsSerializable
還必須是 GWT 允許的 class
此外,自訂的 class 還必須讓在 GWT compiler 會處理的目錄下
不然實際跑起來就會有(很難看懂的)錯誤訊息

其二,呼叫完 msa.shoot() 之後
下一步並不會執行 onSuccess()/onFailure()
更正確來說,在寫 client 端程式碼的時候
並不會知道 onSuccess()/onFailure() 什麼時候會呼叫到
端看 server 處理以及網路傳輸的速度... etc
這是 callback 的特性,在踩地雷的 case 當中並不會造成困擾
但在其他實務上,如果發現怎麼 RPC 回傳值都不正確
那大概就是忘記這個性質所導致的...... Orz

喔對... 都忘記講一個大前提了
要用 GWT RPC,server 必須是 JSP container

同時也來講講 GWT RPC 的好處 \囧/
一言以蔽之就是「通通傳便便」
client 不用組 query string 或 post 內容
server 端不用準備對應的 url
client/server 都不需要剖析傳遞的資料
甚至可以傳遞(客製化的) exception
在 client 端的 onFailure() 可以分門別類處理......
程式碼看起來、寫起來都很 OO、都很 Java
以一個 Java Programmer 來說,有什麼比這個更快樂的事情呢? XD
(連 xml 都沒有用到呢! [握拳])

好了,「對抗電腦版的踩地雷」拆解到這裡
只剩下地雷區要設定 click 的 handler
收到 GameInfo 之後要更新畫面
以及電腦 AI 設計這些功能
相信你一定可以自己寫的很開心的,就跳過不細談了

如果你想偷懶想拿寫好的程式碼來執行看看,
可以到這裡下載。也歡迎投稿你的 AI 設計

Enjoy GWT and have fun! \囧/

2010-08-23

對戰吧~GWT 踩地雷! [上]

上一次教完如何寫一個生命遊戲(好久以前啊 [遠目])
這次的題目同樣是陣列系的踩地雷
不過,如果單機自己玩也太無聊了點
所以呢... 這次的目標是「對抗電腦版的踩地雷」!
這個題目有點大,讓我們一步一步慢慢來......

首先是先弄出一個 MineGM 的物件
來負責創造地雷世界、運作規則邏輯
所以 MineGM 必須要有這些 field
public static final int UNKNOW = -1;

private int x;
private int y;
private int total;  //總共幾個地雷
private int remainder;  //剩下幾個地雷
private boolean[][] answer; //地雷分佈圖
private int[][] map;  //玩家看到的地圖
private int[] playerHit = new int[2]; //分別踩了幾個

一開始就用亂數把 answer 準備好
至於 map 的內容一開始都是 UNKNOWN,表示還不知道是啥狀況
map 當中還可能出現:
  • 0~8:九宮格內出現的地雷數
  • 9:玩家踩到的地雷
  • -9:電腦踩到的地雷
而 MineGM 還需要有一個 public 的 method「shoot()」
負責接受玩家的輸入、然後回報是否命中
public boolean shoot(int hitX, int hitY, boolean who){
  map[hitX][hitY] = count(hitX, hitY);

  //踩到空地的連鎖反應
  if(map[hitX][hitY]==0){
    for(int i=-1; i<2; i++){
      if(hitX+i==x || hitX+i<0){continue;}
      for(int j=-1; j<2; j++){
        if(hitY+j==y || hitY+j<0){continue;}
        if(map[hitX+i][hitY+j] != -1){
          continue;
        }else{
          shoot(hitX+i, hitY+j, who);
        }
      }
    }
  }

  //不同人踩到地雷要給不同值
  if(map[hitX][hitY]==9){
    remainder--;
    if(who){
      playerHit[0]++;
    }else{
      map[hitX][hitY]=-9;
      playerHit[1]++;
    }
  }
  
  return Math.abs(map[hitX][hitY])==9;      
}

count() 會計算周圍九宮格有幾個地雷
把回傳質設定到對應的 map 上
另外,因為我採取「answer 的周圍多一格空地」的作法
所以「踩到空地的連鎖反應」那段的迴圈可以比較好看一點

雖然已經有 MineGM 建立、維護地雷世界了
但是,我們還是需要另外一個 GameInfo 來包裝給玩家的資訊
不然如果玩家 or 電腦直接拿 MineGM 的 answer 來作弊怎麼辦? Orz
所以在 MineGM 當中弄了一個 static method 來轉換成 GameInfo
public static GameInfo toGameInfo(MineGM server) {
  GameInfo result = new GameInfo();
  result.setMap(server.getMap());
  result.setRemainder(server.remainder);
  result.setTotal(server.total);
  result.setPlayerHit(server.playerHit);
  return result;
}

至於其他的細節就留給大家慢慢寫了......

接下來處理 UI 的部份
這次使用 GWT 2.0 的 UiBinder 來處理排版
使用方法可以看官方文件或痞子版的中文翻譯

預計的遊戲畫面長這樣:

上方是數據區
左右兩側是雙方的名字與分數,包了一個 PlayerInfo 來處理
其實很簡單,剛好適合拿來了解 UiBinder

PlayerInfo.ui.xml:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
  xmlns:g="urn:import:com.google.gwt.user.client.ui">
  <ui:style>
  .title{
    padding-left: 5px;  
  }
  </ui:style>
  <g:FlowPanel>
    <g:InlineLabel ui:field="title" styleName="{style.title}"></g:InlineLabel>
    <g:InlineLabel ui:field="hitCount"></g:InlineLabel>
  </g:FlowPanel>
</ui:UiBinder> 

PlayerInfo.java
public class PlayerInfo extends Composite {

  private static PlayerInfoUiBinder uiBinder = GWT.create(PlayerInfoUiBinder.class);

  interface PlayerInfoUiBinder extends UiBinder<Widget, PlayerInfo> {
  }

  @UiField Label title;
  @UiField Label hitCount;
  
  public PlayerInfo() {
    initWidget(uiBinder.createAndBindUi(this));
    setHitCount(0);
  }

  public void setHitCount(int i) {
    hitCount.setText(""+i);
  }

  public void setName(String name){
    title.setText(name+":");
  }
}

中間是還剩下多少地雷,用一個 Label 解決
下方的地雷區則是用 FlexTable 處理
這些東西都放在 MineMain 這個 class 當中
所以 MineMain.ui.xml 會長成這樣:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
  xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:m="urn:import:org.psmonkey.product.client.mine">
  <ui:style>
  .playerInfo{
    width: 480px;
  }
  .cpu{
    width: 220px;
    background-color: red;
  }
  .player{
    width: 220px;
    background-color: #64A0C8;
  }
  .remainder{
    width: 40px;
    color: white;
    background-color: gray;
    text-align: center;
  }
  </ui:style>
  <g:VerticalPanel>
    <g:HorizontalPanel styleName="{style.playerInfo}">
      <m:PlayerInfo ui:field="cpu" styleName="{style.cpu}"></m:PlayerInfo>
      <g:Label ui:field="remainder" styleName="{style.remainder}"></g:Label>
      <m:PlayerInfo ui:field="player" styleName="{style.player}"></m:PlayerInfo>
    </g:HorizontalPanel>
    <g:FlexTable ui:field="map"></g:FlexTable>
  </g:VerticalPanel>
</ui:UiBinder> 

很懶惰地用 VerticalPanel 跟 HorizontalPanel 解決 XD

接下來,就要處理跟 web server 之間的溝通了! [待續]

2009-12-22

用 GWT 寫個「生命遊戲」吧!

原文寫於 Java 版(ptt.cc)

以 GWT 教學系列來說,應該先講環境建制之類 blahblah
不過還是跳過那些繁文縟節,先講比較實在的部份
如果這樣子寫 JavaScript 的方式沒辦法引起興趣
還是會想要用 jQuery 之類的方法來寫程式
那開發環境安裝起來再快(例如 XAMPP [大笑]),也沒啥意義 :P
------------

寫一個 JavaScript 版的生命遊戲(Game of life)
要花多久? 有多難呢?

嗯? Java 版不是說好不提 JavaScript 嗎?
好好好,那拿出以前交作業的 Java 程式吧! [咦?]

首先包個「細胞」class
狀態只有兩種,要嘛活著要嘛死掉
不過得多個變數預知下一代會生會死
public class Cell{
  private boolean now;
  private boolean next;
  public Cell(){}

  public Cell(boolean alive) {now = alive;}

  public boolean isAlive(){return now;}

  public void setNextAlive(boolean alive){
   this.next = alive;
  }

  public void nextTurn() {now = next;}
 }
然後再來包個「世界」的 class
裡頭有一堆細胞,所以用個二維陣列來表示
至於 width, 跟 height 這兩個 field 純粹只是為了懶惰好看 [逃]
public class World{
  private int width;
  private int height;
  private Cell[][] cell;

  public World(int w, int h){
   width = w;
   height = h;

   cell = new Cell[w][h];
   for(int i=0; i<2; i++){
    tmpW = w+i;
    if( tmpW<0 || tmpW==width){
     continue;
    }
    for(int j=-1; j<2; j++){
     tmpH = h+j;
     if( tmpH<0 || tmpH==height){
      continue;
     }
     if(tmpH==h && tmpW==w){
      continue;
     }
     if(getCell(tmpW, tmpH).isAlive()){
      sum++;
     }
    }
   }

   switch(sum){
   case 0: case 1: case 4: case 5:
   case 6: case 7: case 8:
    getCell(w, h).setNextAlive(false);
    break;
   case 3:
    getCell(w, h).setNextAlive(true);
    break;
   }
  }

  public Cell getCell(int w, int h){
   return cell[w][h];
  }
 }
好了,物件都包好了,那麼現在該來解決畫面了
這次不用 Swing、也不用 SWT,試試傳說中的 GWT [奸笑]

「監視器」本身是個 panel,至於 vertical 在幹麼就跳過。
監視器要監看生命遊戲的世界,還要有一個棋盤來顯示狀況
所以 field 大概就是這樣啦~
public class Monitor extends VerticalPanel{
    private World world;
    private Grid board;
    private int width = 10, height = 10;
}
constructor 要把「世界」跟「棋盤」正式建立起來
「棋盤」要放在「監視器」上頭,還要有個按鈕讓人家可以點下一步
所以...
public Monitor(){
  world = new World(width, height);

  board = new Grid(width, height);
  this.add(board);

  Button next = new Button("下一世代");
  //點下一步要幹些什麼好事?
  next.addClickHandler(new ClickHandler(){
    @Override
    public void onClick(ClickEvent event) {
     nextTurn();
    }
  });
  this.add(next);

  //重新整理畫面
  refresh();
 }
這邊先跑出來兩個新的 method
nextTurn() 沒啥好講的,就是要 world 過個年,然後重新整理畫面
public void nextTurn(){
  world.nextTurn();
  refresh();
 }
至於 refresh(),我打算讓活著的細胞用「X」來表示
死掉的細胞的空空白白的什麼都沒有(其實是全形空白)
private void refresh() {
  for (int i = 0; i < width; i++) {
   for (int j = 0; j < height; j++) {
    if (world.getCell(i, j).isAlive()) {
     board.setText(i, j, "X");
    } else {
     board.setText(i, j, " ");
    }
   }
  }
 }
剩下的就是找到啟動的地方
在 GWT 裡頭是 EntryPoint 這個家族的 class 然後這樣寫:
public class Enter implements EntryPoint {
  public void onModuleLoad() {
   RootPanel.get().add(new Monitor());
  }
 }
好啦~ 恭喜你,只剩下 compile 以及找個網頁空間放 .js 檔
JavaScript 版的生命遊戲就... 寫 完 啦... \囧/

輕不輕鬆? 開不開心? [扭扭]
以前的 Java 程式碼可以直接拿來用...


阿? 什麼?
GWT 的開發環境要怎麼安裝?
程式碼要放在哪個目錄下?
怎麼指定 EntryPoint?
ㄜ... 請參照課本第 43 頁就可以了,好! 下課! [逃]

2009-12-03

GWT:這是什麼?幹什麼的?

就像看電影會先看預告片、買 A 片會先看封面
學 GWT 之前,先知道 GWT 是什麼? 能做什麼?
再來決定要不要學,這十分重要

生命中有這麼多美好的事情(例如 H-Game [誤])
不知道為了什麼而學(無趣的)新技術
實在是浪費時間虛擲生命

所以,GWT 是什麼呢?

他是 Java 界的救星
他是 JavaScript 界的剋星
更是網頁開發界的啤兒綠茶... \囧/


好好好,我言歸正傳就是了... [被毆飛]

〔GWT 是什麼?〕
是 Java、也是 JavaScript [扭]

GWT 全名是 Google Web Toolkit
望文生義,是用在網頁上頭的工具組

以軟體開發的角度,GWT 的核心是一個 Java compiler
處理 Java 程式碼,但輸出的不是 byte code
而是在 browser 上頭執行的 JavaScript

因此,為了方便開發時的測試與除錯
GWT 有 hosted mode,包括一個 JSP container 跟 browser
在 hosted mode 下,可以在 runtime 獲得一些錯誤訊息與資訊
這些訊息是由 GWT 提供,會比較好讀、貼近程式碼
(如果你想倚賴 browser 或 FireBug 來 debug,忘了他們吧! XD)
也能點出一些 GWT 才會有的錯誤
例如在 client 端的程式用了 java.util.Hashtable


以軟體架構的角度,GWT 的程式碼
分為 client 端以及可有可無的 server 端

client 端的基礎是 JSNI(JavaScript Native Interface)
Java 無法涵蓋 JavaScript 的部份
尤其是 browser 層級的操作
可以用 JSNI 的寫法包成 Java 可呼叫的 method

GWT 提供基本開發需要的 class 與相關機制
例如 UI componet、event handling、Http wrapper、JSON parser...
已經把 JSNI 包的差不多了
也許你寫完一個 project 都還看不到 JSNI 的影子

server 端的部份,主要是 GWT RPC
RPC 原文是 Remote Procedure Call
簡單地說,這讓你可以幾乎毫無感覺地使用遠端的 method/object
這裡的「遠端」當然是指 web server

GWT RPC 把 HTTP 的傳輸過程包起來
要傳遞的資料,封裝跟 parser 的手續也包起來
不管是在寫 client 端還是 server 端
都不用煩惱這些繁文縟節(而且,都是用 Java 寫!)
Java Compiler 還幫你作 type checking

真是太棒了阿... 這麼好的東西我以後用不到怎麼辦 <囧>

等等... 前頭不是說:server 端是可有可無的嗎?

是的,用 GWT RPC 有一個前提,server 必須是 JSP container
如果你是用 PHP 或是其他語言
又或著你想使用 JSON、XML 的方式來傳遞資料
那麼,GWT RPC 是可以完全不去理會的

〔GWT 能做什麼?〕
client 端? server 端? 傻傻的不用分清楚!

純粹以 JavaScript 的角度來說
JavaScript 能做到的「效果」(不是語言特性 XD)
都能以 Java code 寫出來
在某些情況,(大多是沒有 library 幫你包好 囧>)
才可能需要寫 JSNI 來做到底層控制

以網頁開發的觀點
GWT 能讓你以傳統 application 的思維來面對網頁程式

以 browser 的角度來看,GWT 用 JavaScript 操作 DOM
來產生 & 控制所有的畫面
(再囉唆一次,寫的時候還是用 Java XD)
因此當畫面改變時,頁面不用切換,變數也就不用傳遞
所以,你不用思考 JavaScript 變數要怎麼傳到下一個畫面
也就不用煩惱到底要塞 session 還是跟著 URL 帶著走

如果你用 GWT RPC,那更徹底
除了 callback 的寫法比較怪了點
其餘的部份,你很難分辨是在寫 Java application 還是在寫網頁
object 內容的維持、與 server 的同步也不再是瑣事 or 難事

對於會寫 Java Application 的人而言
要跨進 web 開發的領域
不用把思路切換成一頁一頁的方式、
不用學一堆新技術新語言、
不太需要知道哪些是 server/client 處理
甚至感覺不到 stateless
以開發的角度,這不是很美妙的一件事情嗎?

註:這裡不討論安全性的議題
(迷之聲:因為有人不會 [指])


〔GWT 不能做什麼?〕
不能讓你寫 JavaScript... [誤]

JavaScript 作不到的事情,GWT 也作不到
雖然這根本就像在說廢話 XD
但是當你在撰寫 client 端的程式時
得時時刻刻想起這句話
不然,寫到忘我的時候
很順手的就想讀個 File、連個 DB
Eclipse 這類的 IDE 也不會出現 compile 錯誤
(在 GWT compile 時還是會炸出錯誤訊息)


我研究 GWT 還不是很透徹、其他 JavaScript 的技術也沒在碰
所以 GWT 這個架構不能做什麼? 我實在回答不出來 [毆飛]

這邊只能提出一個「可能性」
就是 GWT 要整合其他 JavaScript 的技術
也許很困難 or 不可能?
之前要嘗試用 jQuery 將 GWT 生出來的 TextArea
裝上彩色模擬 scroll bar 就屢戰屢敗 Orz
(這跟當時懵然無知也很有關係 [炸])
換個角度想,用 GWT 是一定可以做出同樣效果
只是目前可能還沒有人寫 or release 出來

跟 jQuery 比起來,base on GWT 的 UI component(包含 3rd party)
的確是很量少虛弱又天生難看(靠自己的化妝技術彌補)
但這只能算是暫時的缺點,不能算是「作不到的事情」


〔為甚麼要用 GWT〕
因為這是一種 Rock'n Roll 的 style!

如果你喜歡一個 project 同時有好多技術
如果你喜歡自己掌握 HTTP/HTML/JavaScript 的所有細節

或著,你討厭 Swing 那類的 UI code
又甚至,你壓根討厭 Java 的一切

那麼,千萬、千萬不要碰 GWT

反過來說,如果你跟我差不多笨(好可憐阿 [炸])
對 Java 死心塌地,寫起來如吃飯睡覺一般自然
那麼,GWT 會是一個契機、一個救贖、一個捷徑

好了,接下來就要開始進入正題了......

2009-11-18

GWT:序章—用? 不用? Java 的錯?

我注意到 GWT 是今年年初的事情
如果再不碰 AJAX 的技術,就準備骨灰罈把自己裝進去吧![炸]

同時進了一家小工作室,於是跌跌撞撞
把開發環境從 PHP(CodeIgniter)+jQuery
先用 GWT 換掉 jQuery 以及畫面輸出,完成了兩個案子
最後是用純 GWT 完成了一個案子... 然後離職了 lol

雖然都沒用什麼高深的技術、案子也都不大
但也勉強能算是實做過
因此,我一直對 GWT 在台灣(或華文世界?)討論 & 資源很少
甚至連掀起一陣熱潮都沒有(例如像 Ruby,至少出了一堆中文書)
總覺得哪裡怪怪的

如果以 ptt.cc 當樣區
是說,除了 jQuery 之外,其他 AJAX/JavaScript的技術
Ext、YUI、dojo、DWR... 也沒看到什麼討論
Java 版上頭也幾乎是基礎的 J2SE 問題涵蓋了 80% 以上的討論
JSP 的問題也幾乎都「只是」JSP 的問題
zk, JSF 還有一堆東西,幾乎都不見蹤影

這會不會是 ptt.cc 的一種趨勢,除了西瓜效應之外
也停滯不前,或說對新技術的不在意 or 不敏感?
亦或是高手們覺得孤掌難鳴曲高和寡,問/講了也是白問/講?

ㄜ... 扯遠了...

最近沒有工作,加上這兩篇的 combo 連擊 XD

→《Is GWT the future of web development?》
翻譯:《GWT 是網頁開發的未來嗎?》

→《Lost in Translation or Why GWT Isn't the Future of Web Development》
翻譯:《轉換不完全—為甚麼 GWT 不會是網頁開發的未來呢?》
以下簡稱《Isn't Future》

讓我興起想要寫 GWT 推廣文系列的念頭
(迷之聲:系列? 誰知道你能撐幾篇... [毆])

在以傳統教學方式介紹 GWT 之前
先針對反對 GWT 的論點作一些思考
畢竟,知道敵人(?)怎麼想,是很重要的....
(雖然我覺得《Isn't Future》寫的實在... Lost in Words? lol)

OK! Let's start...


*   *   *


〔Java 是快過時的東西嗎?〕
GWT 是以 Java 語法作為開發語言
我們在寫 GWT 時,絕大多數是在寫 Java
只有在需要直接操作 JavaScript 時
才需要藉由 JSNI 來寫一些 JavaScript

《Isn't Future》強調 script 才是未來的主流
type safety 沒有也沒差
dynamic typing 可以少掉很多麻煩
JavaScript、Ruby、PHP 等等語言也寫出很多軟體、也沒有炸掉
在開發階段可以少些時間在等待

(這篇文章看到後來,比較像是在詰譙 Java Orz)

我的程度無法討論 or 比較這些東西誰優誰劣
或著說,我的程度只能寫 Java
因為 Eclipse 這種等級的 IDE 可以幫我處理很多雜事
code assist 也好、open declaration 也好(撇開 OO 的部份)
至少我年初用 PDT 跟很久之前用 RDT
都沒有堪用有效的工具(據說是很難有?)

當然,script 語言如果封裝的好、能夠產生良好的 API doc
那麼這些東西或許就沒差? 充其量就是腦袋要記得多一些東西?

但是,如果回歸商人的角度去想
為甚麼 Google 在選擇 GWT、Android 的開發語言
是用 Java 而不是用 Python?(即使是 App Engine 後來也?)
或是,理論上更多人會用的 C/C++?

話說回頭,「潮流」這回事情實在令人費解
很久以前的 Basic 系列都不用定義變數型態
後來 VB 可以不用定義、也可以強制定義、到後來就都要了
現在有人說,script 才是未來的主流
這中間的思維轉折,到底是怎麼一回事呢?

〔為甚麼我要學 JavaScript?〕
假設你會機械語言 [笑]
你會用機械語言當作軟體開發的語言嗎?

《Isn't Future》的答案是:
你應該要用機械語言;而不是用高階語言撰寫,再透過工具轉換成機械語言
中心思想是 Joel 的文章《The Law of Leaky Abstractions》
試圖論述一件事情:Java 不可能完整地轉成 JavaScript
這又可以拆成兩個部份來討論
一個是實作的能力、一個是語言本身的能力

關於「實做的能力」,《Isn't Future》沒有明確地說些什麼
我想,如果 Google Wave (GMail?)是用 GWT 做出來
這部份大概也沒什麼好質疑的
理論上 JavaScript 能作的事情,GWT 都(可以)有對應的功能
(還有 JSNI 這個大絕招嘛... [炸])

更進一步說,如果你用 GWT(剛好又會 Java 跟 Swing 基本概念 :P)
你將可以擺脫傳統寫網頁程式的困擾:要會一堆東西
(X)HTML、JavaScript、DOM、AJAX...
最後還來個該死的瀏覽器差異
也不用為了要處理上頭那一堆東西,再去學一個像 jQuery 的東西
(jQuery 的程式碼看起來像另一種語言 XD)
你就是在寫 Java,只不過出來的畫面是在 browser 上

當然,我認同文中說「好的程式設計師應該能很快的學會新東西」
但是,開發階段最痛苦的是那些藏在細節中的妖魔鬼怪
這跟學習能力好壞與否無關
唯有對該技術的深入瞭解才能稍微減輕 or 減少這種痛苦
我這種笨蛋,沒辦法掌握太多(各自獨立)的技術

《Isn't Future》大多數是在闡述很多 JavaScript 的語言特性
在 Java 當中是沒有的,而且有些根本作不出來。

問題是:為甚麼? (盧廣仲:Rock!! [炸])

如果功能都做得到,那麼語言特性真的有那麼重要嗎?
我們真的需要在 object 上頭隨意加掛 attribute 跟 method 嗎?
我們真的需要寫 first class function 嗎?

如果不是沒有也無損功能實做(最多囉唆點 :P)
那麼 Java 不能做出 JavaScript 的語言特性,又如何?
現在大多數的語言也沒有 goto 或是 jump 啊?

嗯... 還好我不會機械語言 :)

〔其他〕 上頭沒涵蓋的零散論點,集合在這裡
→GWT 內建 widget 醜到爆
如果你在意自己長得不夠美
只要體質好、本錢夠,整形技術可以讓你煥然一新

只要在 gwt.xml 拿掉設定,widget 預設的 CSS 就會移除
還原成 browser 最原始會呈現的樣子
要自己加掛 CSS name 當然沒有問題,剩下來是美術人員煩惱的事情

反過來說,我還沒看過什麼正式的網站
不去修改元件的外觀(Google 自己的網站不算 XD)

愛美不是罪,但是本末倒置是一種錯誤。

→compile 速度爆慢
GWT compile 成 JavaScript 的時候,真的是打混摸魚的好時光
但是在「全部使用 GWT」的情況下
開發階段根本不需要 compile

client 的變動,在 hosted browser reload 之後
跟改 PHP/JSP 然後重新 reload 的速度差不多
(重點是 GWT 只有「一頁」,PHP/JSP 有好多頁)

server 端的變動,需要 restart server
這跟改 web.xml、servlet 的狀況是一樣的
(GWT RPC,server 端是一個 servlet)
跟 GWT 的 compiler 沒啥關係

只有在 server 端不是使用 GWT RPC
因為 cross-domain 的關係,必須先 compile 成 JavaScript
放到實際 web server 上頭才能測試
(當時有弄出用 form submit 來暫時代替的奧步,可省下一些時間 XD,不知道有沒有其他方法?)
GWT 2.0 會對此做出改進
但是對純 Java(GWT)開發環境而言,其實就還好...

〔結尾 & 開始〕
《Isn't Future》並沒有說服我
還是選擇站在 GWT 這邊的  \囧/

接下來,我會開始唬爛 GWT
盡量有系統地瞎掰出一些教學文章

如有謬誤之處,還請高手前輩們指點一二以正方家
如有疑問 or 建議,也歡迎版上討論 or 來信指教


就先這樣啦~ 囧>

2009-10-07

GWT Animation 再探

在〈GWT Animation 初探〉當中,程式已經能讓畫面看起來有動畫的效果,「表面上」要怎麼使用 Animation 是沒有問題了;不過這樣子有些無趣,還是要殺進去 Animation 來瞭解這一切背後的內幕(?)。

起點當然是 Animation.run(),沒有呼叫這個 method,是不會有什麼反應的。run() 有兩個,run(int duration, double startTime) 的 duration 是 Animation 預計持續作用的時間;startTime 是預計執行的時間。為甚麼用 startTime 的 data type 是 double 呢?這點在 Animation 不算是有用到的 Duration.elapsedMillis() 可以找到答案:
Returns the same result as System#currentTimeMillis(), but as a double. Because emulated long math is significantly slower than doubles in web mode, this method is to be preferred.
因為在瀏覽器上頭,使用 double 處理起來比模擬 long 還要快得多(btw... 為甚麼 Animation 只用了 Duration.currentTimeMillis() 取得時間,而沒有用 Duration.elapsedMillis() 去計算時間差,這我一直想不透 XD)。另一個 run(int duration) 其實還是呼叫 run(int, double),只是自動以當下時間傳給 startTime。

回到 run(int, double) 的內容,關鍵點在於下面這段
if (animations == null) {
   animations = new ArrayList();  //point-A
   animationTimer = new Timer() {
    @Override
    public void run() {
     updateAnimations();
    }
   };
  }
  animations.add(this);
這邊要回頭看一下 Animation 的資料結構。講起來有點饒舌。大致上來說,Animation 有一些 static 的 field 跟 method,目的是統一處理系統當中所有的 Animation object(程式碼 point-A)。這裡也可以看到,其實 Animation 裡頭是用 Timer 來實做的。Timer 的細節得先跳過,這裡只要知道看到 animationTime.schedule(int delayMillis) 就表示隔了 delayMillis 個 ms 就會執行 updateAnimations() 的內容,而 updateAnimations() 會呼叫 update()。那麼,勢必有需要好好看一下 update() 的內容:
private boolean update(double curTime) {
  boolean finished = curTime >= startTime + duration;
  if (started && !finished) {
   // Animation is in progress.
   double progress = (curTime - startTime) / duration;
   onUpdate(interpolate(progress));
   return false;
  }
  if (!started && curTime >= startTime) {
   // Start the animation.
   started = true;
   onStart();
   // Intentional fall through to possibly end the animation.
  }
  if (finished) {
   // Animation is complete.
   onComplete();
   started = false;
   running = false;
   return true;
  }
  return false;
 }
裡頭依照不同的狀況,呼叫了 onUpdate(), onStart() 跟 onComplete()。嗯?為甚麼只有 onUpdate() 是 abstract 的呢?因為這兩個到最後還是去呼叫 onUpdate(),progress 的值給 0 表示剛開始、給 1 表示結束。接下來就是最詭異的部份啦,傳給 onUpdate() 的數值,居然還要經過 interpolate() 的計算,這又是為甚麼呢?根據 javadoc 的說法:
Interpolate the linear progress into a more natural easing function.
看個對照圖可能比較好懂:

在開始跟結束的部份比較緩和,或許這樣比較符合人類視覺觀點?總之,這是為甚麼 Animation 的 javadoc 會說「at a non-fixed frame rate」了。(順帶提一點,相同的 duration,呼叫 onUpdate() 的次數應該會一樣,但是 progress 值會有差異。這應該是 Timer 先天上無法很精準的缺陷?)

看到這邊,Animation 應該可以說沒有秘密了。剩下來就是如何運用的問題了...... [遠目]

2009-10-05

GWT Animation 初探

Animation 是 GWT 內的一個 class,名字取的很美妙,實際上是在做什麼的呢? GWT 的 showcase 給了一個示範,不過裡頭的程式碼實在有點古怪,姑且直接來看 1.6 版的 API doc 怎麼說。
An Animation is a continuous event that updates progressively over time at a non-fixed frame rate.
哈哈... 根本就是騙人的,哪來什麼動畫 [笑]。簡單地說「Animation 是一個會持續要求 update 動作的物件。而間隔的時間是不固定的。」阿?這什麼鬼?還是用程式碼來說明好了...

import com.google.gwt.animation.client.Animation;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Label;

public class HelloAnimation extends AbsolutePanel{
 private Label hello = new Label("Hello Animation");
 private final int WIDTH = 800;
 private final int HEIGHT = 200;
 
 public HelloAnimation(){
  this.setPixelSize(WIDTH, HEIGHT);
  this.add(hello); //要先加上去,才有辦法移動
  Player player = new Player(this);
  player.run(5*1000); //point-A
 }
 
 public void playOneFrame(double progress){
  this.setWidgetPosition(hello, (int)(WIDTH*progress), HEIGHT/2);
 }
}

class Player extends Animation{
 HelloAnimation target;
 
 public Player(HelloAnimation t){
  target = t;
 }
 
 @Override
 protected void onUpdate(double progress) {
  target.playOneFrame(progress);
 }
}

只要 new 一個 HelloAnimation,然後加到 RootPanel 上頭去,就會看「Hello Animation」從畫面左邊跑到右邊。(至於最後「Hello Animation」突然換行的狀況,先跳過... XD)

裡頭兩個 class 分別負責兩件事情。HelloAnimation 負責畫面顯示的部份,直接 extends AbsolutePanel,搭配 AbsolutePanel.setWidgetPosition() 就可以很快速地設定 widget 的位置,這樣在上頭的東西「動」起來就相對方便。playOneFrame() 這個 method 就是在處理這件事情,至於 progress 這個參數的作用,就必須回到 Animation(也就是範例程式裡頭的 Player)才能說明。

Player 這個 class 其實很簡單,extends Animation 之後,只有一個 method 必須實作,就是 onUpdate()。這個 method 就是 API 提到的效果,當 Player 起作用的時候,onUpdate() 就會持續地被呼叫、並且給予不同的參數 progress 值,值域介於 0~1 之間,會隨著時間會越來越大,乘上 100 就是完成度啦,因為實際負責「動」的物件是 HelloAnimation,所以在呼叫 HelloAnimation.playOneFrame() 時候就原封不動地傳進去。這個參數在製作像動畫這種東西時,就很方便,因為 Animation 幫你算好 progress,不用自己動手。像這個例子當中,「Hello Animation」要在指定的時間內橫越指定的寬度,但是在撰寫 playOneFrame() 時完全不用理會「指定的時間」有多長,只要把寬度乘上 progress 就解決了。現在動畫的長度是五秒鐘(程式碼 point-A),要改成兩倍慢 or 兩倍快,就只要在 player.run() 時給 10 秒 or 2.5 秒就可以了。

看到這裡,是不是覺得 GWT 設計的很好、使用起來很簡單呢? [奸笑]

至於 Animation 的細節,我們下次再談(初探嘛...)

2008-09-02

從傳統語言轉到 OOP 的輔助文章

發表於某站某版... 貼過來增加文章數目 [茶]
(不然... 委靡不振的 AdSense 收入阿阿阿阿... [遠目])

我不是 OO 強者,我都是硬幹的寫 OO [挺]
所以,我沒辦法回答你太高深的問題
只能分享一下 n 年前從 QB 轉到 Java 來的時候跨過的障礙.... [遠目]

首先,以某種角度而言,OO 裡頭的 method 跟平常的 function...
棍... 根本沒什麼差別... Orz

反正需要的話,參數還是要傳、該 return 的還是要 return
在同一個 class 裡頭互相用的 method 更是有這種感覺
(前頭不用寫 xxxx.fooMethod(),只要寫 fooMethod() 就可以用啦 XD)

棍... 那差別在哪?

不如來看字串... 如果你要把一個字串裡頭所有的 ps 都變成 87
傳統 function 情況下大概是這樣寫...

String aString = "I am ps";
String bString = "ps is 87";
aString = replace(aString, "ps", "87");
bString = replace(bString, "ps", "87");

function replace(sourceString, targetString, replaceString){
//跳過,要 return 新字串就是了
//要不 return 也可,不過那跟 oo 比較沒啥關係,就跳過
}

用 OO 的寫法... 喔... 因為 String 這個 class Java 已經提供了
所以直接看用法,那會是

String aString = "I am ps";
String bString = "ps is 87";
aString = aString.replace("ps", "87");
bString = bString.replace("ps", "87");

基本上,正常人(或是像我這種異常人 XD)會覺得 OO 的寫法比較好讀

不過,光是這樣好像也沒什麼
那麼,來設計一下橋牌的遊戲

如果在傳統(沒有 OO)的寫法大概要這樣寫

//第一個是哪一個玩家,第二個是 13 張排,後面一個塞花色一個塞點數
int card[][][] = new int[4][13][2];

//每個人的墩數,英文應該叫做 trick 吧? (錯了就算了)
int trick[] = new int[4];

//其他懶得寫了,現在已經沒辦法用這種方法思考
//請自行想像發揮,然後跟後頭 OO 版本的坐個對應

可是 OO 的話,大概會寫成

//包 card 物件
class Card{
int number;
int flower; //花色 lol
//getter 跟 setter 之類就請自便
}

//包玩家
class Player{
Card card[] = new Card[13];
int trick;

//每一輪第一個,可以隨便出
public Card aHand(){
//blahblah
}

//不然就會被限定花色
public Card aHand(int flower){
//blahblah
}

//贏了一墩
public void winTrick(){
trick++;
}
}

//遊戲流程大概就會長成這樣
Player player[] = new Player[4];
Card trickCard[] = new Card[4];
int startPlayer; //紀錄這一輪由誰開始

//發牌叫牌就跳過 [茶]

trickCard[0] = player[startPlayer].aHand();
for(int i=1; i<4; i++){
trickCard[i] =
player[(startPlayer+i)%4].aHand(trickCard[0].getFlower());
}

startPlayer=this.getTrickWiner();
player[startPlayer].winTrick();

private int getTrickWiner(){
//根據 trickCard 跟 startPlayer 寫一堆判斷之類 blahblah
}

上面講的這些,大概是封裝 encapsulation 的範疇
如果之前已經寫過不少程式,那麼有了這個基礎(或是跨過這個障礙)
再看其他 OO 的東西,或許會比較快樂一點

是說,我也只會這個 [遠目]

2007-03-18

快快樂樂學 RMI - Start!

身為一個只會寫 Java 的人而言,遇到分散式系統,好像不能不實際來 try 一下 RMI。不過對於用 Eclipse 習慣的人而言... 寫 RMI 實在好麻煩啊... [泣],google 找到的 plug-in 似乎只有這個,但是看到他要收費,馬上就軟掉(?)了。Ant 又完全沒有用的習慣,還是回憶一下 bat 檔怎麼寫吧... Orz

好了,言歸正傳。

因 為種種點點點我不知道的原因(理論上是因為 Java 沒有多重繼承,然後 RMI 又希望溝通的物件是 parent class 是 java.rmi.server.UnicastRemoteObject,不過這純屬猜測... API 懶得看 XD),所以真正要讓 client 可以遠端呼叫的物件,必須讓它 implement 自己訂做的 interface,而且這個 interface 還得 extend java.rmi.Remote。所以,就先來定義一下 interface 吧:

FooInterface.java
package test.remoteInterface;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.Date;

public interface FooInterface extends Remote {
public Date getServerDate() throws RemoteException;

public String getServerMessage() throws RemoteException;

public void setServerMessage(String s) throws RemoteException;
}


這 裡留了幾個惡搞的空間。只要 Server 跟 client 不同機器,且時間調的不一樣,那麼 getServerDate() 就可以顯現出差異。而 serverMessage 的 setter 跟 getter 是用來測試 server 端的物件如果被改變,能不能 keep 住。下面這個實做就變得沒啥好講了... [茶]

FooObject.java
package test.remoteObject;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Date;
import test.remoteInterface.FooInterface;

public class FooObject extends UnicastRemoteObject implements FooInterface {
/**
* 為了減少 warning 而加的
*/
private static final long serialVersionUID = 1L;

private String message;

public FooObject() throws RemoteException {
super();
message = "Default Message";
}

public Date getServerDate() throws RemoteException {
return new Date();
}

public String getServerMessage() throws RemoteException {
return message;
}

public void setServerMessage(String s) throws RemoteException {
message = s;
}
}


然後追加一個 server,執行這個就可以讓他接客了 \囧/
FooServer.java
package test;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import test.remoteObject.FooObject;

public class FooServer {
public static void main(String[] args) {
FooObject foo;
try {
foo = new FooObject();
Naming.rebind("rmi://127.0.0.1/FooServer", foo);
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}
System.out.println("Foo Server is ready");
}
}


最後,當然是來寫 client 端啊:
FooClient.java
package test;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.Date;
import test.remoteInterface.FooInterface;

public class FooClient {
/**
* 1: ip 位址
* 2: 設定字串
* @param args 參數
*/
public static void main(String[] args) {
try {
FooInterface foo = (FooInterface) Naming.lookup("rmi://" + args[0]
+ "/FooServer");
System.out.println("Server Date : " + foo.getServerDate());
System.out.println("Client Date : " + new Date());
System.out
.println("Before set message : " + foo.getServerMessage());
foo.setServerMessage(args[1]);
System.out.println("After set message : " + foo.getServerMessage());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
} catch (NotBoundException e) {
e.printStackTrace();
}
}
}
給 我等一下![指],並不是這樣子 compile 完,分別執行 FooServer 跟 FooClient 就能用。你還得針對 FooObject 另外作 rmi compile 的處理。在這個 foo 的例子當中,就要下(classpath 等問題請自己搞定)
rmic test.remoteObject.FooObject
這會另外產生 FooObject_Stub.class,在包 client 的執行環境,這個檔也要加進去。

另外,在執行 FooServer 之前,得先執行一個程式 rmiregistry(在 {JDK}/bin/)。如果你也不幸是用 Windows,那在 command line 的環境下打「start rmiregistry」,會另外自動開個黑窗。

一切都順利的話,就可以來真正來玩玩看了。[茶]

錯誤排除區:
java.net.ConnectException→咪的,rmiregistry 啟動了沒?

備註區:
網路上的範例幾乎都有在 client/server 端的程式碼來這行:
System.setSecurityManager(newRMISecurityManager())
但是... 我用了反而問題一大堆(有設定 java.policy 了),還搞出一堆靈異現象。拿掉那行反而一點事情也沒有 [爆]。