【Effective Java】項目20:タグ付クラスよりクラス階層を選ぶ

クラスが2つ以上の特性を持っている場合、タグフィールドを保持するよりも、クラス階層を利用するべき。

タグ付クラス

タグ付クラスとはクラスが2つ以上の特性を持っていて、タグフィールドを保持することで、それらを区別するように実装されているクラスのことである。

public class Figure {
    enum Shape { RECTANGLE, CIRCLE }
    
    // 図形タイプを保持する
    final Shape shape;
    
    // shape が RECTANGLE の場合だけ利用する 
    double length;
    double width;
    
    // shape が CIRCLE の場合だけ利用する
    double radius;
    
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }
    
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }
    
    double area() {
        switch (shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError();
        }
    }
}

このようなタグ付クラスは以下の様な欠点を持つ。

  • 複数の実装が単一クラスにまとまって可読性が低い
  • 利用している特性以外のためのフィールドも保持するので、メモリ使用量が増える
  • タグフィールドの初期化をクラスの開発者が行う必要があり、間違えて初期化すると実行時エラーが発生する
  • ソースファイルにタグを追加することでしか、新たな特性が追加できない
    • ソースファイルの変更が必要
    • その都度、メソッド内に case 文が必要になる
  • 特性の違いが型に反映されない

クラス階層による置き換え

タグ付クラスパターンで実装した Figure クラスを、クラス階層によって置き換えると以下のようになる。

abstract class Figure {
    abstract double area();
}

public class Circle extends Figure {
    final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

public class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    @Override
    double area() {
        return length * width;
    }
}

このパターンは上述した欠陥をすべて克服している。

  • 複数の実装が異なるクラスで実装されている
    • 可読性が向上している(凝集度が高い)
  • 単一の特性のみインスタンス化されるため、利用しないフィールドは存在しない
    • メモリ使用量が低くなる
  • タグフィールドが存在しないので、タグフィールドの初期化を考える必要がない
  • ソースファイルに変更を加えることなく、拡張という方法で新たな特性を追加できる
  • 特性の違いが型に反映されており、コンパイル時の型検査が有効になる

なお、上述した例ではフィールドが直接アクセスされているが、クラスが public の場合には private にしてアクセッサを提供するべきである(項目14

感想

ひさしぶりの Effective Java シリーズだ。 実際にはすでに下書きしていたので、ざーっと読み直して校正しただけなのだが。 とりあえず年内に第4章の「クラスとインタフェース」は終わらせよう。

そういえば、さんざん拡張は駄目って言われてたわけで Figure は interface 使った方がいいんじゃない?って思ったけど、どうなんでしょうね。