5.バリューオブジェクトの実装例
前ページ゛では、バリューオブジェクトの実装の戦略について考えました。この戦略に沿って実際にJavaのバリューオブジェクトを実装してみましょう。
5.1 Point
2次元空間上で点の位置を表現するための“データ型”Pointの実装はリスト1です。
import java.io.Serializable; import java.util.regex.Pattern; import java.util.regex.Matcher; // [推奨]finalにして、不用意なサブクラスが作成されることを防ぐ // [推奨]Cloneableをimplementsして複製可能とする // [推奨]Serializableをimplementsして直列化可能とする public final class Point implements Cloneable, Serializable { // [必須]インスタンス変数をprivateにし、インスタンス変数がクラス外から // 変更されることを防ぐ // [推奨]インスタンス変数をfinalにし、コンストラクタ以外から // 値が設定されることを防ぐ private final int x; private final int y; // [必須]値を指定したコンストラクタ public Point(int x, int y) { this.x = x; this.y = y; } // [参考]文字列からデータを生成するコンストラクタ public Point(String point) { Pattern pattern = Pattern.compile("<point x='([0-9.]+)' y='([0-9.]+)'/>"); Matcher matcher = pattern.matcher(point); if (matcher.matches()) { x = Integer.parseInt(matcher.group(1)); y = Integer.parseInt(matcher.group(2)); } else { throw (new IllegalArgumentException(point)); } } // [参考]他のPointと同じ値のデータを生成するコンストラクタ public Point(Point point) { this.x = point.x; this.y = point.y; } // [推奨]データの文字列表現を生成 public String toString() { return ("<point x='" + x + "' y='" + y + "'/>"); } // [必須]値としての比較を行う public boolean equals(Object another) { if (!(another instanceof Point)) { return (false); } Point point = (Point)another; return (point.x == x && point.y == y); } // [推奨]複製を生成 public Object clone() { return (new Point(x, y)); } // [必須]データの内容を通知 public final int getX() { return (x); } // [必須]データの内容を通知 public final int getY() { return (y); } }
5.1.1 必須
バリューオブジェクトとして重要な以下の性質を実現することが必要です。
- イミュータブルオブジェクト
- 内容による比較
以上の性質を実現するために「必須」となるのは、以下の項目です。
- インスタンス変数をprivate指定
- データの内容を通知するアクセサ
- 値を指定したコンストラクタ
- 値の比較を行うequalsメソッド
前述したように「変数への代入に特別な配慮がいらないこと」はイミュータブルオブジェクトによって実現することになります。
イミュータブルオブジェクトの実現は、「インスタンス変数をprivate指定」「データの内容を通知するアクセサ」「値を指定したコンストラクタ」の3つの実装により、オブジェクトの生成時に設定した値が絶対に変更されないことを保証することによって行われています。
まず、インスタンス変数をprivate指定することで、ほかのオブジェクトが直接この変数を変更する可能性をゼロにします。データの内容を通知するアクセサ(getメソッド)はサポートするものの、データの内容を更新するミューテータ(setメソッド)はサポートしないことで、メソッドを経由してデータの更新を行うことを不可能にします。そして、オブジェクトの生成時にデータの設定を行えるようにするために、コンストラクタでデータを指定できるようにしています。
以上の実装により、新しく作成したクラスがイミュータブルオブジェクトとなります。
「内容による比較ができること」は「内容の比較を行うequalsメソッド」によって実現します。これは、オブジェクトを構成するすべてのインスタンス変数を丁寧に比較することで実現されています。
5.1.2 推奨
実装を推奨するのは、以下の項目です。いずれも、バリューオブジェクトとしての使いやすさを追求した機能です。
- クラスのfinal指定
- Cloneableのimplements
- 複製を生成するcloneメソッド
- Serializableのimplements
- インスタンス変数のfinal指定
- データの文字列表現を通知するtoStringメソッド
「クラスのfinal指定」は、サブクラスの作成を不可能にするものです。この指定を行うと拡張性は乏しくなりますが、“サブクラスがイミュータブルでないオブジェクトとして実装されてしまう”という事故を防ぐことができます。
「Cloneableのimplements」は、なくても実害はないですが、データを表現するオブジェクトの場合には実装可能なはずなので、実装しておいた方が得という観点から推奨しています。また、併せて「複製を生成するcloneメソッド」の実装も行っておきます。
「Serializableのimplements」は、オブジェクトを直列化可能にします。データを表現するオブジェクトの場合には直列化が可能なはずなので、実装しておいた方が得という観点から推奨しています。プログラムの手間もinterfaceを1つimplementsするだけでほとんどかかりません。Cloneableの場合は、すぐ分かる具体的なメリットはありませんが、Serializableの場合にはRMIの引数に使えるオブジェクトになるという重要なメリットがあります。
「インスタンス変数のfinal指定」は、コンストラクタ以外からデータの変更を行うことが不可能になります。正しく実装されている場合には、この指定がなくてもインスタンス変数の更新は行われないわけですが、コンパイラに変更が不可能なことを保証してもらうという意味でできれば宣言しておいた方がよいでしょう。
「データの文字列表現を通知するtoStringメソッド」は、少なくともデバッグに有効なので、実装しておくことを推奨します。また、実装においては、この例で行っているようにXMLの形式を用いると応用の範囲が広がります。
5.1.3 参考
そのほか、以下のものを参考として実装しました。
- 文字列からデータを生成するコンストラクタ
- ほかのPointと同じ値のデータを生成するコンストラクタ
「文字列からデータを生成するコンストラクタ」は、外部ファイルに格納されている情報からオブジェクトを生成したい場合に重宝するコンストラクタです。特に前節で推奨したtoStringメソッドの出力をそのまま入力にできると、応用範囲が広がります。本格的に作るなら、SAXやDOMといったXMLパーサを使うのがよいでしょう。ここではJ2SE 1.4でサポートされた正規表現を使ってお手軽な実装にしています。
「ほかのPointと同じ値のデータを生成するコンストラクタ」は、いわゆるコピーコンストラクタと呼ばれているものです。オブジェクトPointはイミュータブルオブジェクトであり、またCloneableでもあるので、コピーコンストラクタがなくても困ることはありませんが、余裕があれば作っておいてもよいでしょう。
5.2 値として操作してみる
それでは、前回テストに使用したSpaceShipの例で、バリューオブジェクトバージョンのPointの動作を検証してみましょう。SpaceShipはリスト2となります。
public class SpaceShip { private Point position; public void move(Point position) { this.position = position; } public boolean isHit(Point position) { return (this.position.equals(position)); } public void print() { System.out.println("x = " + position.getX() + ", y = " + position.getY()); } }
動作確認のためのプログラムはリスト3となります。
第5回のリスト9のClientでは、moveメソッドを呼び出した後に、Pointオブジェクトの内容を変更しており、これがSpaceShipのデータが破壊された原因でした。それに対して、このバージョンのPointオブジェクトはイミュータブルオブジェクトなので変更することはできません。このため、moveメソッドの引数として渡されたPointを値と同様に扱っても問題が発生しないことが保証されているわけです。変更できないオブジェクトというのは一見効率が悪そうに感じますが、どのようなバグがあっても絶対にデータが破壊されないということでもあります。この性質、つまりイミュータブルであることは極めて重要な性質なのです。
public class Client { public static void main(String[] args) { SpaceShip ship = new SpaceShip(); Point point1 = new Point(100, 200); ship.move(point1); // point1を変更することはできない ship.print(); Point point2 = new Point(100, 200); boolean isHit = ship.isHit(point2); System.out.println(isHit); } }
それでは実行してみましょう(実行結果1)。
$ java Client x = 100, y = 200 true
無事動作しましたね。
Copyright © ITmedia, Inc. All Rights Reserved.