2006 年 12 月 22 日 22 時 40 分

ProtocolVersionMessage クラス


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


Message インタフェースを利用して、
前述の ProtocolVersion メッセージをクラス化してみよう。

========== ProtocolVersion.java ==========

package jp.loafer.rfb.message;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jp.loafer.rfb.RFBContext;
import jp.loafer.rfb.io.RFBInputStream;
import jp.loafer.rfb.io.RFBOutputStream;

/**
* ProtocolVersion メッセージ。
* @author kes
*/
public class ProtocolVersionMessage implements Message {
   
    /**
     * 永続化用空コンストラクタ。
     */
    public ProtocolVersionMessage() {
        // デフォルト値で初期化
        this(3003); // version 3.3
    }

    /**
     * ProtocolVersion メッセージを作成。
     * @param version RFB バージョン。
     */
    public ProtocolVersionMessage(int version) {
        this.version = version;
    }

    /**
     * RFB バージョンを取得。
     * @return RFB バージョン。
     */
    public int getVersion() {
        return version;
    }

    /**
     * @see Message#read(RFBContext, RFBInputStream)
     */
    public void read(RFBContext context, RFBInputStream in) throws IOException {

        // クライアント
        byte buffer[] = new byte[12];
        in.readFully(buffer);
       
        String message = new String(buffer, "us-ascii");
        Pattern p = Pattern.compile("^RFB (\\d{3})\\.(\\d{3})\n$");
        Matcher m = p.matcher(message);
        if (!m.matches()) throw new IOException("RFB Protocol violation.");
       
        int major = Integer.parseInt(m.group(1));
        int minor = Integer.parseInt(m.group(2));

        version = major * 1000 + minor;
    }

    /**
     * @see Message#write(RFBContext, RFBOutputStream)
     */
    @SuppressWarnings("boxing")
    public void write(RFBContext context, RFBOutputStream out) throws IOException {
        int major = version / 1000;
        int minor = version % 1000;
        String message = String.format("RFB %03d.%03d\n", major, minor);
        out.write(message.getBytes("us-ascii"));
    }
   
    private int version;

}

========== end of ProtocolVersion.java ==========

このクラスはメッセージの構造や読み書き処理を、
read/write メソッドに隠蔽している。
Message インタフェースの規約どおり、
空のコンストラクタも持たせてある。

ProtocolVersion メッセージが持つ意味のある値は、
メジャーとマイナーのバージョン値だけなので、
利用側から見るとこの値のみを持つクラスに見える。

メッセージを送信する側は、まずメッセージを作成し、
バージョン値の設定を行う必要があるので、
これはコンストラクタで渡せるようにしておいた。
メッセージが完成したら、write を呼んで「送信」する。

String#format は Java-1.5 で追加されたメソッドだ。
C の printf を意識して作られており、
.NET の String#Format のような機能を持っている。

format メソッドの第 2 引数は Java-1.5 の可変長引数だ。
自前で Object[] を作る手間を省き、
任意の数のオブジェクト型を順番に渡すことができる。

上記では何とプリミティブである int を渡しているが、
Java-1.5 以降では、ボクシング(Boxing)が機能し、
自動的に Integer クラスへ変換されるため問題ない。
.NET に慣れている俺にはありがたい機能だ。

なお、既知の警告を抑制するために、
@SuppressWarnings アノテーションを追加している。
暗黙変換が嫌いな方は Integer.valueOf 等を呼べば良い。

なお、Java-1.4 以前で実装する場合は、
DecimalFormat("000") 等でごにょごにょやる事になる。

さて、メッセージを受信する側は、read で「受信」する。
read の実装はテスト時のコードそのままだ。
バージョンを解析してフィールドに格納することで、
ストリームからインスタンスを復元する作業となる。

後は、フィールドのアクセサだが、
これは、getVersion だけにしておこう。

setVersion を用意しない理由は、
setter のない不変クラスにしておきたいからだ。
といっても、永続化の仕組みがある以上完全ではないが、
read メソッドさえ封印すれば不変クラスとして使える。

クラスが不変か可変かは、かなり重要な要素となる。
不変クラスは値が変更されないことが保障されるので、
安心してインスタンスをメソッドに渡すことができる。
また、スレッドセーフに実装するのも簡単となるのだ。

逆に、setter や状態を変更するメソッドを持つクラスは、
常時インスタンスの状態を変更することが可能である。
これら可変クラスのインスタンスを、
適当なメソッドに渡した後には、
インスタンスの状態が変更される可能性もある。

これら可変クラスの寿命が長い場合は、
どこでクラスの値が変更されるか分からないため、
慎重に設計する必要が生じるのだ。

特に、可変クラスのインスタンスをフィールドとする場合、
その変数をそのまま返す getter を作るのには検討が必要だ。
クラスの役割によっても違うが、状況によっては、
変更させないようにクローンの生成を要する事もある。

ということで、setter は最小限にすることにしよう。
その方が見通しが良くなると考えられるのだ。
まあ、必要が生じれば後で setter を追加すればいいしね。

では、ProtocolVersionMessage を使って、
RFBSession#execute を書き直してみよう。

========== RFBSession#execute ==========

        // 通信コンテキストを作成
        RFBContext context = new RFBContext();

        // ProtocolVersion ハンドシェイク
       
        // サーバのバージョンを送信
        ProtocolVersionMessage serverVersion
                = new ProtocolVersionMessage(3008); // version 3.8
        out.writeMessage(context, serverVersion);
        out.flush();

        // クライアントのバージョンを受信
        ProtocolVersionMessage clientVersion
                = in.readMessage(0, ProtocolVersionMessage.class);
       
        int major = clientVersion.getVersion() / 1000;
        int minor = clientVersion.getVersion() % 1000;

        System.err.printf("Client version: %d.%d\n", major, minor);

========== end of RFBSession#execute ==========

非常に見通しが良くなったことが分かる。
クラスの復元も readMessage 一発でできるのは楽だ。
readMessage が限定的なクラス型を引数に取るため、
String や Object 等のクラス型を渡す危険性はない。

このクラス型引数は「<? extends Message」 と、
上限境界ワイルドカードを指定しているため、
Message インタフェースを実装したクラスしか受取らない。

しかし……Version を int にしたのは失敗かもな。



Copyright (c) 1994-2007 Project Loafer. All rights reserved.