ISBN を例として不変クラスを作ってみる (1)

Effective Java で説かれているのが、不変クラスの重要性だ。以前、読書メモを以下の記事に書いた。

不変クラスとは、インスタンス生成後は保持データの変更ができないクラスのことだ。例えば、Java の String や基本データクラスは不変クラスだ。ある String のインスタンスが持つ文字列を変更することはできない。

不変クラスは主に以下の点で便利だ。

  • 単純で扱いやすい
  • マップやセットなど、他のオブジェクトで利用しやすい
  • スレッドセーフである

あるクラスを不変クラスとするためには、いくつかの条件がある。それを説明するために、ISBN を例として Java で不変クラスを作ってみる。本記事では、

  • ISBN
  • 不変クラスの条件

について説明する。

ISBN とは

ISBN は書籍を識別するための番号だ。本の裏のコレである。

f:id:kymmt90:20150125113423j:plain

詳しくは Wikipedia を参照してほしい。

今回は現行規格である 13 桁 ISBN を保持するクラスを作る。ISBN クラスには以下のメソッドを実装する。

  • String で渡した ISBN からインスタンスを生成するメソッド
  • 接頭、グループ、出版社、書名、チェックディジットの各部分を取得するメソッド
  • ある String が ISBN として正当かどうか確かめるメソッド
  • ある 10 桁 ISBN を 13 桁 ISBN に変換するメソッド

ISBN 自体はただの数字の羅列であり、状態を持たない。よって、クラスとして実現するのであれば、不変クラスとすべきだろう。

不変クラスの条件

ISBN を不変クラスとして作っていくために、不変クラスの条件について説明する。条件は以下の通り。

  1. setter のような、オブジェクトの状態を変更できるメソッドを持たない
  2. サブクラスで状態を変更されないように、クラスの拡張を防ぐ
  3. すべてのフィールドの可視性を private にする
  4. final をつけるなどしてすべてのフィールドを変更不可にする
  5. 可変オブジェクトを持つ場合、その不変クラスだけが変更できるようにする

各項目とも、外部からの変更を防ぐことを目的としたものである。項目 2 と 5 について、注意点を説明する。

2. クラス拡張禁止について

クラスの拡張を防ぐ方法として、Java であれば、以下のように final を利用できる。

public final class Isbn {
//...

これ以外にも、コンストラクタを private にする方法がある。コンストラクタが private だと、そのクラスを継承したとき、その子クラスから親クラスのコンストラクタを呼び出せないため、拡張できない。

しかし、そのままだと、その不変クラスを生成できない。そこで、以下のような static ファクトリメソッドを定義する。

public class Isbn {
    // static ファクトリメソッド(単純な例)
    public static Isbn of(String arg) { return new Isbn(arg); }
    // private なコンストラクタ
    private Isbn(String arg) {
        //...
    }
}

これで、Isbn.of("978-4-***-*****-*") のようにすることでインスタンスを生成できる。static ファクトリメソッドのほうが柔軟性は高いので、今回はこの方法をとる。static ファクトリメソッドの詳細は以下の記事を参照してほしい。

5. 可変オブジェクトについて

可変オブジェクトを外部から変更されないようにするためには、防御的なコピーが必要となる。

ISBN とは異なるが、例えば Person クラスを名前、誕生日をフィールドに持つ不変クラスとしたいときを考える。誕生日には可変クラス Date を利用する。このとき、コンストラクタや getter を以下のように実装してはいけない。

public final class Person {
    private final String name;
    private final Date birthday;

    // ダメなコンストラクタ
    public Person(String name, Date birthday) {
        this.name = name;
        this.birthday = birthday;
    }

    // ダメな getter
    public Date birthday() { return birthday; }
}

なぜなら、このクラスを利用するクライアントから、以下のように date を(Date.setMonth は非推奨メソッドながら)操作できてしまい、Person が不変クラスとして成り立たなくなってしまうからである。

DateFormat format = new SimpleDateFormat("yyyy/MM/dd");
Date date = format.parse("1940/10/09");
Person john = new Person("John Lennon", date);

// Wed Oct 09 00:00:00 JST 1940
System.out.println(john.birthday());

// 外部から john の内部を操作
date.setMonth(0);

// Tue Jan 09 00:00:00 JST 1940
System.out.println(john.birthday());

これを防ぐために、以下のように防御的にコピーするのが重要である。

public final class Person {
    public Person(String name, Date birthday) {
        this.name = name;
        // 防御的コピー
        this.birthday = new Date(birthday.getTime());
    }

    public Date birthday() {
        // 防御的コピー
        return new Date(birthday.getTime());
    }
}

これで、Person の内部と外部で Date インスタンスが別々のものとなり、外部から birthday を操作できなくなるため、不変クラスといえることになる。

とはいえ、今回の ISBN では可変オブジェクトを持たせない予定なので、この条件については気にしないことにする。

次回

不変クラスの条件を守った ISBN のクラスを作ってみる。

参考文献

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)