jmake-proxy可以把Java介面方法的實作方式和函式本身分開,並透過lisp-this傳入狀態。implementation可以是函式、hash-table或package;本文以按鈕計數為例,逐步示範這幾種寫法,以及如何依方法名稱分派實作。
在第一個互動元件:Hello Button & Action中,使用jinterface-implementation建立JavaFX的EventHandler:
(jinterface-implementation
"javafx.event.EventHandler"
"handle"
(lambda (event)
(format t "Action for Event ~A~%" event)))
除了jinterface-implementation外,ABCL還提供jmake-proxy。它同樣可以建立符合EventHandler的物件交給JavaFX使用,但可以讓我們更靈活地決定如何實作介面。
Java介面會宣告必須實作的方法規格。例如,JavaFX的EventHandler要求實作handle(Event):
public interface EventHandler<T extends Event> {
void handle(T event);
}
Button.setOnAction要求一個EventHandler物件:
button.setOnAction(handler);
這也是我們在jinterface-implementation使用的形式:
(jinterface-implementation
"javafx.event.EventHandler"
"handle" (lambda (event) ......))
不過,這種寫法把介面方法直接對應到函式;當實作需要保存自己的狀態時,就不夠方便。例如,以下要計算按鈕被按下的次數(make-btn-cnt-action-handler):
(defun create-count-scene ()
(prog* ((btn-cnt (new "javafx.scene.control.Button" "Count"))
(box (new "javafx.scene.layout.VBox" 50.5 (jnew-array-from-list (jclass "javafx.scene.Node")
`(,btn-cnt))))
(result (new "javafx.scene.Scene" box 640 480)))
(#"setAlignment" box (jfield "javafx.geometry.Pos" "CENTER"))
(#"setOnAction" btn-cnt (make-btn-cnt-action-handler))
(return result)))
可以透過閉包保存計數狀態:
(defun make-btn-cnt-action-handler ()
(let ((cnt 0))
(jinterface-implementation
"javafx.event.EventHandler"
"handle" (lambda (event)
(#"show" (make-messagebox
(format nil "Clicked ~A times."
(incf cnt))))))))
不過,使用閉包意味著必須把實作細節放在函式內部,不方便另外抽出單純的#'handle函式:
(defun make-btn-cnt-action-handler ()
(let ((cnt 0))
(jinterface-implementation
"javafx.event.EventHandler"
"handle" #'handle)))
若要將#'handle獨立出來,就必須改變它的參數形式,至少額外傳入一個可變狀態:
- (defun handle (event) ......)
+ (defun handle (state event) ......)
而jmake-proxy允許這種形式的改變。它的基本形式是:
(jmake-proxy interface implementation &optional lisp-this)
interface:Java介面名稱,或Java class reference。implementation:負責實作介面方法的物件。ABCL接受函式、hash-table或package。lisp-this:傳給實作函式的額外物件,可用來保存代理物件的狀態;省略時是NIL。最後一個參數lisp-this就是傳給#'handle的狀態物件。
(defun make-btn-cnt-action-handler ()
(let ((state (list :click-count 0))) ;; state 需要可變的
(jmake-proxy
"javafx.event.EventHandler"
#'handle
state))) ;; 傳遞給實現
傳入的是可變的 plist,因此可以透過getf讀取和更新狀態:
(defun handle (state method event)
(declare (ignore method event))
(#"show"
(make-messagebox
(format nil "Clicked ~A times."
(incf (getf state :click-count)))))) ;; 取得&變更狀態
上面的實作除了event以外,還有一個未使用的method參數;它代表目前呼叫的方法名稱。Java介面不一定只有一個方法,EventHandler只是要求實作handle(Event)。因此,可以先檢查方法名稱,再執行對應邏輯:
(defun handle (state method event)
(declare (ignore event))
(if (string= method "handle")
(progn
(#"showAndWait" (make-messagebox "Yes, it is handle"))
(#"show"
(make-messagebox
(format nil "Clicked ~A times."
(incf (getf state :click-count))))))
(#"show" (make-messagebox "No, it is not handle"))))
除了依據不同的method執行不同邏輯(類似 switch-case),也可以建立上層路由,分派各方法的實作:
(defun method-route (state method &rest args)
(cond
((string= method "handle")
(apply #'handle state args))
(t (prog nil
(#"show" (make-messagebox "No Method Found"))))))
這麼一來#'handle就可以不需要判斷method,將參數移除:
(defun handle (state event)
(declare (ignore event))
(#"show"
(make-messagebox
(format nil "Clicked ~A times."
(incf (getf state :click-count)))))) ;; 取得&變更狀態
#'make-btn-cnt-action-handler改用分派器method-route:
(defun make-btn-cnt-action-handler ()
(let ((state (list :click-count 0)))
(jmake-proxy
"javafx.event.EventHandler"
#'method-route ;; 換成使用分派器
state)))
hash-table 或 package以上示範了以單一函式作為implementation的方式。實際上,jmake-proxy是泛型函式(generic function),implementation也可以是hash-table或package。
hash-table使用hash-table時,不必自行實作method-route分派器;ABCL會依照方法名稱,在傳入的hash-table中尋找對應的函式。因此只要準備好hash-table即可:
(defvar *btn-cnt-action* (make-hash-table :test #'equal)) ;; 建立 hash-table
(setf (gethash "handle" *btn-cnt-action*) ;; 設定 handle 對應的函式
#'handle)
#'jmake-proxy就可以直接使用定義的hash-table -- *btn-cnt-action*:
(defun make-btn-cnt-action-handler ()
(let ((state (list :click-count 0)))
(jmake-proxy
"javafx.event.EventHandler"
*btn-cnt-action* ;; 換成使用hash-table
state)))
package使用package時,可直接在指定的package中定義對應函式。由於前面已定義#'handle,也可以直接傳入目前的*package*:
(defun make-btn-cnt-action-handler ()
(let ((state (list :click-count 0))) ;; state 需要可變的
(jmake-proxy
"javafx.event.EventHandler"
*package* ;; 當前package
state)))
不過,函式參數更合適的形式是(lisp-this &rest args)。我嘗試省略&rest時會報錯,因此需要改成以下形式:
(defun handle (state &rest event) ;; 調整增加&rest
(declare (ignore event))
(#"show"
(make-messagebox
(format nil "Clicked ~A times."
(incf (getf state :click-count))))))
或是為了避免符號衝突,建立一個獨立的:btn-handler package:
(defpackage :btn-handler
(:use :cl)
(:export :handle))
(in-package :btn-handler)
(defun handle (state &rest event)
(declare (ignore event))
(#"show"
(abcl-javafx::make-messagebox
(format nil "Clicked ~A times."
(incf (getf state :click-count))))))
(in-package :abcl-javafx) ;; 回到:abcl-javafx使用
(defun make-btn-cnt-action-handler ()
(let ((state (list :click-count 0)))
(jmake-proxy
"javafx.event.EventHandler"
(find-package :btn-handler) ;; 使用 :btn-handler
state)))
我測試後發現,
package和hash-table形式也會傳入方法名稱,只是文件中的函式簽名沒有列出。前面的範例忽略了event,所以不影響執行;若要同時處理方法名稱和其餘參數,可以使用&optional搭配&rest:(defun handle (state &optional method &rest event) (declare (ignore method event)) (#"show" (make-messagebox (format nil "Clicked ~A times." (incf (getf state :click-count))))))
最後可以建立Stage來檢視看看:
(let* ((scene (create-count-scene))
(stage (new "javafx.stage.Stage")))
(#"setScene" stage scene)
(#"show" stage))

本文示範了三種implementation形式:函式適合自行分派方法,hash-table可直接以方法名稱查找函式,package則可利用其中已定義的函式。需要讓代理物件保存資料時,透過lisp-this傳入狀態即可;可以從按鈕計數範例開始,依介面複雜度選擇實作方式。