クラスが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 使った方がいいんじゃない?って思ったけど、どうなんでしょうね。