JavaFXでクラサバする事を考えた

私はエスアイヤーでギョームアプリ作ってるのでその辺りにJavaFXぶっ込んだらどうなるか考えてみました。

とりあえず次に挙げた機能が必要っぽいかなーと思います。

  • 画面遷移
  • 検索結果などのグリッド表示
  • グリッドから詳細画面を開く的なやつ
  • ダイアログ
  • バックグラウンド処理
  • サーバとの通信

画面遷移

Hoge駆動の忘年会というぁゃしぃ集まりでは

という案を挙げてみました。 このふたつのうちSceneを入れ替える方法だと画面がチラついてしまいましたので ルートノードを入れ替える方法がベターかなー、と思っていましたが、 StackPaneあたりに必要なだけNodeを突っ込んでvisibleを切り替える、 というのがもっと良いんじゃないかと現時点では思っています。

package sample;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ScreenTransitionSample extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {

        final BorderPane firstPane = new BorderPane();
        Button next = new Button("次へ行く");
        firstPane.setCenter(next);

        final BorderPane secondPane = new BorderPane();
        Button prev = new Button("前へ戻る");
        secondPane.setCenter(prev);
        secondPane.setVisible(false);

        StackPane root = new StackPane();
        root.getChildren().add(firstPane);
        root.getChildren().add(secondPane);

        next.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                firstPane.setVisible(false);
                secondPane.setVisible(true);
            }
        });
        prev.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                firstPane.setVisible(true);
                secondPane.setVisible(false);
            }
        });

        Scene scene = new Scene(root, 400, 300);
        stage.setScene(scene);
        stage.setTitle("画面遷移");
        stage.show();
    }
}

検索画面などのグリッド表示

TableView を使います。

Scene Builderを使って画面を組み立てる場合はまずTableViewをペタっと置いて、 カラムを足す場合はTableColumnを貼付けたTableViewへペロっと置きます。

データを表示するときは TableColumn#setCellValueFactory(Callback) を使って値のファクトリーを設定して、 TableViewのitemsへaddします。

package sample;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;

public class GridController implements Initializable {

    @FXML
    private TableView<Account> accounts;
    @FXML
    private TableColumn<Account, String> id;
    @FXML
    private TableColumn<Account, String> name;
    @FXML
    private TableColumn<Account, String> desc;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        id.setCellValueFactory(new PropertyValueFactory<Account, String>("id"));
        name.setCellValueFactory(new PropertyValueFactory<Account, String>("name"));
        desc.setCellValueFactory(new PropertyValueFactory<Account, String>("desc"));

        Account account1 = Account.newInstance("backpaper0", "うらがみ", "全裸");
        Account account2 = Account.newInstance("tan_go238", "たんご", "カレー");
        Account account3 = Account.newInstance("irof", "いろふ", "足首");

        accounts.getItems().add(account1);
        accounts.getItems().add(account2);
        accounts.getItems().add(account3);
    }
}

こんな感じになります。

../../../_images/GridSample.png

グリッドから詳細画面を開く的なやつ

TableColumn#setCellFactory(Callback) を使います。 Callback実装クラスではセル毎にcallメソッドが呼ばれるようですが、 ここでTableCellを作成して返します。 TableCellではsetGraphicメソッドでボタンをセットしています。 これでセルにボタンを置く事が出来るようです。

public class GridController implements Initializable {

    ...

    @FXML
    private TableColumn<Account, String> opener;

    @Override
    public void initialize(URL location, ResourceBundle resources) {

        opener.setCellFactory(new OpenerFactory());

        opener.setCellValueFactory(new PropertyValueFactory<Account, String>("id"));

        ...
    }
}

class OpenerFactory implements Callback<TableColumn<Account, String>, TableCell<Account, String>> {

    @Override
    public TableCell<Account, String> call(TableColumn<Account, String> param) {
        TableCell<Account, String> tableCell = new TableCell<Account, String>() {

            private Pane pane = createPane();

            private String id;

            private Pane createPane() {
                HBox pane = new HBox();
                pane.setAlignment(Pos.CENTER);

                Button button = new Button("詳細を開く");
                button.setOnAction(new EventHandler<ActionEvent>() {

                    @Override
                    public void handle(ActionEvent event) {
                        System.out.println(id);
                    }
                });
                pane.getChildren().add(button);
                return pane;
            }

            @Override
            protected void updateItem(String id, boolean empty) {
                super.updateItem(id, empty);
                if (empty == false) {
                    this.id = id;
                    setGraphic(pane);
                }
            }
        };
        return tableCell;
    }
}

ボタンが置けました。

../../../_images/GridOpenerSample.png

ダイアログ

Swingで言うところのJOptionPaneのようなお手軽ダイアログは無いようですが、Stageを使う事で実現可能っぽいです。

バックグラウンド処理

Service を使います。 SwingWorker的なやつです。 こんな感じの雰囲気で。

Service<String> service = new Service<String>() {

    @Override
    protected Task<String> createTask() {
        Task<String> task = new Task<String>() {

            @Override
            protected String call() throws Exception {
                TimeUnit.SECONDS.sleep(5);
                return "終わったよーん";
            }

            @Override
            protected void succeeded() {
                text.setText(getValue());
            }
        };
        return task;
    }
};
service.start();

Service#createTask()でTaskを返しています。 Taskではcallメソッドを実装していますが、これがSwingWorkerでいうdoInBackgroundのようです。 succeededメソッドは処理が正常終了したときに呼ばれます。 他にキャンセルしたときに呼ばれるcancelledメソッドや失敗したときに呼ばれるfailedメソッドがあります。

しかし結果がどうあれ必ず呼ばれるfinally的なメソッドがありません。 これは不便な気がします。

finally的なアレを実現する方法として今んところ思いついているのは、Taskには実行中かそうでないかを表すrunningというbooleanのプロパティがあるので、 それにリスナーを追加します。

task.runningProperty().addListener(
        new ChangeListener<Boolean>() {

            @Override
            public void changed(ObservableValue<? extends Boolean> observable,
                                Boolean oldValue, Boolean newValue) {
                if (!newValue) {
                    System.out.println("終わったよーん");
                }
            }
        });

もっと良い方法があったら教えて欲しいです。

あと、処理中にProgressIndicatorというのを表示しておくと良い感じになりそうです。 StackPaneにメインとなるPaneとProgressIndicatorを含んだPaneを突っ込んでvisibleで切り替えます。

というわけでバックグラウンド処理のサンプルを次に記載します。

package sample;

import java.util.concurrent.TimeUnit;
import javafx.application.Application;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class LongTimeTaskSample extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {

        //インジケータを含むPaneを組み立てる
        final GridPane progressPane = new GridPane();
        progressPane.setAlignment(Pos.CENTER);

        ProgressIndicator indicator = new ProgressIndicator();
        progressPane.add(indicator, 0, 0);

        //メインとなるPaneを組み立てる
        GridPane mainPane = new GridPane();
        mainPane.setAlignment(Pos.CENTER);
        mainPane.setHgap(10);
        mainPane.setVgap(10);

        Button button = new Button("重い処理を行う");
        mainPane.add(button, 0, 0);

        final Text text = new Text();
        mainPane.add(text, 0, 1);

        //StackPaneに突っ込む
        StackPane root = new StackPane();
        root.getChildren().add(mainPane);
        root.getChildren().add(progressPane);

        //最初はインジケータは見えなくする
        progressPane.setVisible(false);

        button.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {

                //ボタンが押されたらインジケータを見せる
                progressPane.setVisible(true);
                text.setText("");
                Service<String> service = new Service<String>() {

                    @Override
                    protected Task<String> createTask() {
                        Task<String> task = new Task<String>() {

                            @Override
                            protected String call() throws Exception {
                                TimeUnit.SECONDS.sleep(5);
                                return "終わったよーん";
                            }
                        };
                        task.setOnSucceeded(new EventHandler<WorkerStateEvent>() {

                            @Override
                            public void handle(WorkerStateEvent event) {
                                //タスクが終わったらインジケータを見えなくする
                                progressPane.setVisible(false);
                                text.setText(getValue());
                            }
                        });
                        return task;
                    }
                };
                service.start();
            }
        });

        Scene scene = new Scene(root, 400, 300);
        primaryStage.setScene(scene);
        primaryStage.setTitle("重い処理");
        primaryStage.show();
    }
}

実行したらこんな感じです。

ポチっと。

../../../_images/LongTimeTaskSample1.png

くるくるー。

../../../_images/LongTimeTaskSample2.png

どーん!

../../../_images/LongTimeTaskSample3.png

ちなみにこのコードだとインジケータが表示されているときにボタンはクリックできなくなっていますが、 タブでフォーカス移動してスペースキーで押せたりします。 きっとjava.awt.FocusTraversalPolicyのようなものがあると思うのでまた勉強しておくことにします。

サーバとの通信

( ゚∀゚)o彡°JAX-RS!JAX-RS!

まとめ

ここ数日JavaFXを触ってみてエスアイヤーのギョームアプリも普通に書けそうだなー、と感じました。

また今回はフォーカスしませんでしたがFXMLで画面を組めるのがすごく良いですね。 Scene Builderでサクッとモックを作って、OKならそのまま実装する、というスタイルが楽にできそうで嬉しいです。

あとScene Builderが特定のIDEに依存していないのも嬉しいですね。 好きなIDEを使えます。

という訳でJava 8がリリースされたら是非ともJavaFXでアプリケーション組みたいなー、と思ったのでした。

おわり。

参考資料

Visitorパターンについて考えた

というお話。縦に長いです。コードが。

ポリもーなんとかでなんとかする

例えば数値を表すNumNode、足し算を表すAddNode、それらのインターフェースとなるNodeがあるとします。 で、計算を実装する場合Nodeにcalcメソッドとか定義してNumNodeとAddNodeで実装します。

package visitor;

public interface Node1 {

    int calc();
}

class NumNode1 implements Node1 {

    public final int value;

    public NumNode1(int value) {
        this.value = value;
    }

    @Override public int calc() {
        return value;
    }
}

class AddNode1 implements Node1 {

    public final Node1 left;
    public final Node1 right;

    public AddNode1(Node1 left, Node1 right) {
        this.left = left;
        this.right = right;
    }

    @Override public int calc() {
        return left.calc() + right.calc();
    }
}

こいつで2 + 3 + 4を計算する場合は次のように使います。

package visitor;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import org.junit.Test;

public class Node1Test {

    @Test public void testCalc() throws Exception {
        Node1 node1 = new NumNode1(2);
        Node1 node2 = new NumNode1(3);
        Node1 node3 = new AddNode1(node1, node2);
        Node1 node4 = new NumNode1(4);
        Node1 node5 = new AddNode1(node3, node4);
        int actual = node5.calc();
        int expected = 2 + 3 + 4;
        assertThat(actual, is(expected));
    }
}

さて、今度は組み立てたNodeをWriterへ書き出したくなったとします。 Nodeへprintメソッドを追加します。

package visitor;

import java.io.IOException;
import java.io.Writer;

public interface Node2 {

    int calc();

    void print(Writer out) throws IOException;
}

class NumNode2 implements Node2 {

    public final int value;

    public NumNode2(int value) {
        this.value = value;
    }

    @Override public int calc() {
        return value;
    }

    @Override public void print(Writer out) throws IOException {
        out.write(String.valueOf(value));
    }
}

class AddNode2 implements Node2 {

    public final Node2 left;
    public final Node2 right;

    public AddNode2(Node2 left, Node2 right) {
        this.left = left;
        this.right = right;
    }

    @Override public int calc() {
        return left.calc() + right.calc();
    }

    @Override public void print(Writer out) throws IOException {
        out.write("(");
        left.print(out);
        out.write("+");
        right.print(out);
        out.write(")");
    }
}

さて、次は××処理を追加しますのでNodeへ××メソッドを……と、まあ、 これはこれで良いんですが、処理を追加するたびにNodeおよびNode実装クラスに手を加える必要があり、それが嬉しくない状況もあります。

処理を外から渡す

Nodeから処理を取り除いて外から渡すことを考えてみます。

まずはinstanceofで分岐しつつ各Node実装クラスについて処理してみます。

package visitor;

public interface Node3 {}

class NumNode3 implements Node3 {

    public final int value;

    public NumNode3(int value) {
        this.value = value;
    }
}

class AddNode3 implements Node3 {

    public final Node3 left;
    public final Node3 right;

    public AddNode3(Node3 left, Node3 right) {
        this.left = left;
        this.right = right;
    }
}

class Calclurator3 {

    public int calc(Node3 node) {
        if (node instanceof AddNode3) {
            AddNode3 addNode3 = (AddNode3) node;
            return calc(addNode3.left) + calc(addNode3.right);
        } else if (node instanceof NumNode3) {
            NumNode3 numNode3 = (NumNode3) node;
            return numNode3.value;
        }
        throw new IllegalArgumentException(String.valueOf(node));
    }
}

わーいこれで処理を外だしデキタヨー、じゃねえよ!という感じですね。

instanceofの欠点はケースの漏れを静的に検出できないことだと思っています。 例えばこの例で言うとNode実装クラスはAddNodeとNumNodeがありますが、 NumNodeへの分岐を忘れていてもコンパイル時に気付きません。 さらに node instanceof java.util.Date とか無関係なクラスを書いていても これもコンパイル時に気付きません。

ケースの漏れを静的に検出といえばswitchがありますが、 Stringかprimitiveまたはenumしか使えないので今回の例には不適当です。

いやいやそんなのポリもーなんとかでアレすれば良いじゃん、という事で次のようなインターフェースを作ります。

interface Visitor4 {

    int visit(NumNode4 node);

    int visit(AddNode4 node);
}

これをこういう風に実装すれば……

class Calclurator4 implements Visitor4 {

    public int calc(Node4 node) {
        return visit(node);
    }

    @Override public int visit(NumNode4 node) {
        return node.value;
    }

    @Override public int visit(AddNode4 node) {
        return visit(node.left) + visit(node.left);
    }
}

華麗に解決!というわけには行きませんね。 calcメソッド内のvisit(node)やAddNodeをとるvisitメソッド内でのvisit(node.left)やvisit(node.right)では 渡しているNode実装クラスがなんなのか、コンパイル時には分かりませんので普通にコンパイルエラーです。

無理矢理コンパイルを通そうと思うとこんなコードになりました。

class Calclurator4 implements Visitor4 {

    public int calc(Node4 node) {
        if (node instanceof NumNode4) {
            return visit((NumNode4) node);
        } else if (node instanceof AddNode4) {
            return visit((AddNode4) node);
        } else {
            throw new IllegalArgumentException(String.valueOf(node));
        }
    }

    @Override public int visit(NumNode4 node) {
        return node.value;
    }

    @Override public int visit(AddNode4 node) {

        //compile error
        //return visit(node.left) + visit(node.left);

        if (node.left instanceof NumNode4) {
            NumNode4 left = (NumNode4) node.left;
            if (node.right instanceof NumNode4) {
                NumNode4 right = (NumNode4) node.right;
                return visit(left) + visit(right);
            } else if (node.right instanceof AddNode4) {
                AddNode4 right = (AddNode4) node.right;
                return visit(left) + visit(right);
            } else {
                throw new IllegalArgumentException(String.valueOf(node));
            }
        } else if (node.left instanceof AddNode4) {
            AddNode4 left = (AddNode4) node.left;
            if (node.right instanceof NumNode4) {
                NumNode4 right = (NumNode4) node.right;
                return visit(left) + visit(right);
            } else if (node.right instanceof AddNode4) {
                AddNode4 right = (AddNode4) node.right;
                return visit(left) + visit(right);
            } else {
                throw new IllegalArgumentException(String.valueOf(node));
            }
        } else {
            throw new IllegalArgumentException(String.valueOf(node));
        }
    }
}

はい、そこそこクソコードになりましたね? ていうかまたinstanceofが出てきましたし。

そこでVisitorパターンですよ

Nodeにacceptメソッドを追加して実装クラスで対応するvisitメソッドを呼ぶようにします。

package visitor;

import java.io.IOException;
import java.io.Writer;

public interface Node5 {

    int accept(Visitor5 visitor);
}

class NumNode5 implements Node5 {

    public final int value;

    public NumNode5(int value) {
        this.value = value;
    }

    @Override public int accept(Visitor5 visitor) {
        return visitor.visit(this);
    }
}

class AddNode5 implements Node5 {

    public final Node5 left;
    public final Node5 right;

    public AddNode5(Node5 left, Node5 right) {
        this.left = left;
        this.right = right;
    }

    @Override public int accept(Visitor5 visitor) {
        return visitor.visit(this);
    }
}

interface Visitor5 {

    int visit(NumNode5 node);

    int visit(AddNode5 node);
}

class Calclurator5 implements Visitor5 {

    @Override public int visit(NumNode5 node) {
        return node.value;
    }

    @Override public int visit(AddNode5 node) {
        int left = node.left.accept(this);
        int right = node.right.accept(this);
        return left + right;
    }
}

class Printer5 implements Visitor5 {

    private final Writer out;

    public Printer5(Writer out) {
        this.out = out;
    }

    @Override public int visit(NumNode5 node) {
        try {
            out.write(String.valueOf(node.value));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return 0;
    }

    @Override public int visit(AddNode5 node) {
        try {
            out.write("(");
            node.left.accept(this);
            out.write("+");
            node.right.accept(this);
            out.write(")");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return 0;
    }
}

先ほどの例とは異なりvisitメソッドを各Node実装クラスのacceptメソッド内で呼んでいるので どのvisitメソッドなのかコンパイル時に分かりますね。

これは次のように使います。

package visitor;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import java.io.StringWriter;
import org.junit.Test;

public class Node5Test {

    @Test public void testCalc() throws Exception {
        Node5 node1 = new NumNode5(2);
        Node5 node2 = new NumNode5(3);
        Node5 node3 = new AddNode5(node1, node2);
        Node5 node4 = new NumNode5(4);
        Node5 node5 = new AddNode5(node3, node4);
        Calclurator5 calclurator = new Calclurator5();
        int actual = node5.accept(calclurator);
        int expected = 2 + 3 + 4;
        assertThat(actual, is(expected));
    }

    @Test public void testPrint() throws Exception {
        Node5 node1 = new NumNode5(2);
        Node5 node2 = new NumNode5(3);
        Node5 node3 = new AddNode5(node1, node2);
        Node5 node4 = new NumNode5(4);
        Node5 node5 = new AddNode5(node3, node4);
        StringWriter out = new StringWriter();
        Printer5 printer = new Printer5(out);
        node5.accept(printer);
        String actual = out.toString();
        String expected = "((2+3)+4)";
        assertThat(actual, is(expected));
    }
}

これで処理を外に出せました。

が、visitメソッドの戻り値がintだったりそもそもPrinterでは戻り値が意味なかったりしてもやもやしますね。

ジェネリクスを使う

使いましょう。 引数と戻り値をジェネリクスでアレします。

package visitor;

import java.io.IOException;
import java.io.Writer;

public interface Node6 {

    <R, P> R accept(Visitor6<R, P> visitor, P parameter);
}

class NumNode6 implements Node6 {

    public final int value;

    public NumNode6(int value) {
        this.value = value;
    }

    @Override public <R, P> R accept(Visitor6<R, P> visitor, P parameter) {
        return visitor.visit(this, parameter);
    }
}

class AddNode6 implements Node6 {

    public final Node6 left;
    public final Node6 right;

    public AddNode6(Node6 left, Node6 right) {
        this.left = left;
        this.right = right;
    }

    @Override public <R, P> R accept(Visitor6<R, P> visitor, P parameter) {
        return visitor.visit(this, parameter);
    }
}

interface Visitor6<R, P> {

    R visit(NumNode6 node, P parameter);

    R visit(AddNode6 node, P parameter);
}

class Calclurator6 implements Visitor6<Integer, Void> {

    @Override public Integer visit(NumNode6 node, Void parameter) {
        return node.value;
    }

    @Override public Integer visit(AddNode6 node, Void parameter) {
        int left = node.left.accept(this, parameter);
        int right = node.right.accept(this, parameter);
        return left + right;
    }
}

class Printer6 implements Visitor6<Void, Writer> {

    @Override public Void visit(NumNode6 node, Writer parameter) {
        try {
            parameter.write(String.valueOf(node.value));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    @Override public Void visit(AddNode6 node, Writer parameter) {
        try {
            parameter.write("(");
            node.left.accept(this, parameter);
            parameter.write("+");
            node.right.accept(this, parameter);
            parameter.write(")");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return null;
    }
}

これでだいぶ良い感じになってきました。

が、PrinterでIOExceptionをRuntimeExceptionへラップしてるおなじみのコードが哀愁を漂わせます。

例外もジェネリクスで

やってしまいましょう。 Visitorの定義を少し修正します。

package visitor;

import java.io.IOException;
import java.io.Writer;

public interface Node7 {

    <R, P, E extends Exception> R accept(Visitor7<R, P, E> visitor, P parameter)
            throws E;
}

class NumNode7 implements Node7 {

    public final int value;

    public NumNode7(int value) {
        this.value = value;
    }

    @Override public <R, P, E extends Exception> R accept(
            Visitor7<R, P, E> visitor, P parameter) throws E {
        return visitor.visit(this, parameter);
    }
}

class AddNode7 implements Node7 {

    public final Node7 left;
    public final Node7 right;

    public AddNode7(Node7 left, Node7 right) {
        this.left = left;
        this.right = right;
    }

    @Override public <R, P, E extends Exception> R accept(
            Visitor7<R, P, E> visitor, P parameter) throws E {
        return visitor.visit(this, parameter);
    }
}

interface Visitor7<R, P, E extends Exception> {

    R visit(NumNode7 node, P parameter) throws E;

    R visit(AddNode7 node, P parameter) throws E;
}

class Calclurator7 implements Visitor7<Integer, Void, RuntimeException> {

    @Override public Integer visit(NumNode7 node, Void parameter) {
        return node.value;
    }

    @Override public Integer visit(AddNode7 node, Void parameter) {
        int left = node.left.accept(this, parameter);
        int right = node.right.accept(this, parameter);
        return left + right;
    }
}

class Printer7 implements Visitor7<Void, Writer, IOException> {

    @Override public Void visit(NumNode7 node, Writer parameter)
            throws IOException {
        parameter.write(String.valueOf(node.value));
        return null;
    }

    @Override public Void visit(AddNode7 node, Writer parameter)
            throws IOException {
        parameter.write("(");
        node.left.accept(this, parameter);
        parameter.write("+");
        node.right.accept(this, parameter);
        parameter.write(")");
        return null;
    }
}

もうだいぶ訳の分からないクソコードとなってきた感じがしますが、 Calculatorではチェック例外を投げず、PrinterではIOExceptionを投げるような表現が できました。 お疲れ様でした。

他の言語ではどうなのか

Groovyではどのメソッドを呼ぶかは実行時に決まるのでacceptメソッドが不要です。 たしか動的ディスパッチと呼ばれていたと思います。

class AddNode {
    def left
    def right
}

class NumNode {
    def value
}

class Calculator {
    def visit(AddNode node) {
        visit(node.left) + visit(node.right)
    }
    def visit(NumNode node) { node.value }
}

def node1 = new NumNode(value: 2)
def node2 = new NumNode(value: 3)
def node3 = new AddNode(left: node1, right: node2)
def node4 = new NumNode(value: 4)
def node5 = new AddNode(left: node3, right: node4)
def calculator = new Calculator()
def actual = calculator.visit(node5)

assert actual == (2 + 3 + 4)

またScalaではパターンマッチを使えば良いです。 ケースの漏れはsealedを使えば検出可能だったと思います。

sealed trait Node
case class NumNode(value: Int) extends Node
case class AddNode(left: Node, right: Node) extends Node

def calc(node: Node): Int = node match {
  case NumNode(value) => value
  case AddNode(left, right) => calc(left) + calc(right)
}

val node1 = NumNode(2)
val node2 = NumNode(3)
val node3 = AddNode(node1, node2)
val node4 = NumNode(4)
val node5 = AddNode(node3, node4)
val actual = calc(node5)

assert(actual == (2 + 3 + 4))

それに、Nodeを分解してvalueやleft、rightを取り出したりできてvisitorパターンより超高機能です。 しかもコード短いし。 静的なアレだし。

まとめ

Javaではデータとアルゴリズムを分離するとき、

  • instanceofはケースの漏れを静的に検出できない
  • switchはString、primitive、enumしか受け付けない
  • パターンマッチが無い

などの理由によりVisitorパターンを使わざるを得ない場合があります。 しかし数あるデザインパターンの中でもVisitorパターンは理解するのが難しいように思います。 またそれなりに汎用的にしようと思うとジェネリクスを使って複雑な定義になってしまったり。

よって、使わなくて済むならそれに越した事は無く、使う場合でも本当にVisitorパターンが必要なのかしっかり検討すべきだと思います。

私も最近、色々あってVisitorパターンを使ってしまいましたが、もっと良い設計があったような気がしています。

こんなまとめでええんか?

まあいいか。