使用するプログラミング言語で広く受け入れられているコーディング規約にそってコードを書き、クラスを設計するのは大切なことです。また、既存プロジェクトでコードをメンテナンスするような局面では、特異なコーディングスタイルで自分の個性を主張するよりも、既存コードのスタイルや設計方針に従うことが好まれるのがふつうです。(仮に、既存コードのスタイルがそのプログラミング言語の一般的なスタイルから少々離れていたとしても)
しかし時には、小規模な新しいプロダクトのコードや、自分だけの個人プロジェクトでは、少し原則から外れたコーディングスタイルを試してみることがあります。
この記事では、そのような例として「チェーン可能な setter メソッド」を持つように Java クラスを書くスタイルについて紹介します。
普通の Java クラス
Java クラスは、一般的には次のように書きます。
public class Entity {
private int id;
private String name;
private String value;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getResult() {
return String.format("%d:%s -> %s", id, name, value);
}
}
Entity は、単に今回の例のために書いたクラスです。3 個の private フィールド id, name, value を持ち、各フィールドのための getter と setter があり、仮想の「結果」を取得するための getResult() メソッドを持ちます。
(なお、setter と getter を書く順序に関するスタイル、private フィールドを定義する位置に関するスタイル、空行のスタイル、private final フィールドを初期化するための引数をコンストラクタで受け取り setter を設けないことで不変オブジェクトを作ることを好むスタイルなど、こんな小さいクラスについてもいろいろと一般的な議論ができますが、ここではそれらは省略します)
この Entity クラスを使うクライアントコードは、次のようになります。
Entity entity = new Entity();
entity.setId(1);
entity.setName("foo");
entity.setValue("bar");
System.out.println(entity.getResult());
これも単なる例で、Entity オブジェクトを作成後に適当な値で各プロパティを初期化し、最後に「結果」を取得してコンソールに出力しているだけです。
チェーン可能な Java クラス
一般的な Java クラスの書き方から離れ、チェーン可能な setter メソッドを提供するように Entity クラスを書き直してみます。
public class Entity {
private int id;
private String name;
private String value;
public int id() {
return id;
}
public Entity id(int id) {
this.id = id;
return this;
}
public String name() {
return name;
}
public Entity name(String name) {
this.name = name;
return this;
}
public String value() {
return value;
}
public Entity value(String value) {
this.value = value;
return this;
}
public String result() {
return String.format("%d:%s -> %s", id, name, value);
}
}
一般的なクラスの書き方と比べて、以下の点を変更しています。
- 各 setter メソッドの型(戻り値の型)を void から Entity に変更
- 各 setter メソッドの最後で return this する
- 各 getter と setter メソッドの名前から、get と set を除去
setter をチェーン可能にするために必要な変更は、上記の 1 と 2 です。
3 はおまけで、setter メソッドをチェーン可能にすることと直接の関係はありません。しかし、getter が常に get や is で開始し、setter が常に set で開始しなければならないという言語的なルールはありませんし、JavaScript で広く使われた JQuery の例を考えても、開発者たるものが obj.name() と obj.name("foo") のどちらが getter でどちらが setter か区別できないということもないはずです。構文をより簡潔にするため、ここでは 1, 2 と合わせて 3 の変更を適用しました。(ただし注意点もあります。このようなメソッド名は JavaBeans 規約を満たさないため、JavaBeans を期待するツールやフレームワークの中では、このクラスは使いにくくなります)
修正版の Entity クラスを使うと、クライアントコードは次のように書き換えられます。
System.out.println(new Entity().id(1).name("foo").value("bar").result());
あるいは多少改行を入れると、次のようになります。
System.out.println(new Entity()
.id(1)
.name("foo")
.value("bar")
.result());
どうでしょうか。
簡潔で見やすいクライアントコードだと感じるか、1 行ごとに setter を分けてきっちり書くのに比べて乱雑だと思うかは、人それぞれかもしれません。しかし個人的には、リッチコンストラクタを作らなくてもプロパティの初期化を 1 行で簡潔に書け、オブジェクトの作成、初期化、利用を一連の記述にまとめられるため、とても気に入っています。
チェーン可能だからといって、常にチェーンしなければならないわけではないため、次のように書いてもいいのです。
Entity entity = new Entity().id(1).name("foo").value("bar");
System.out.println(entity.result());
return this; の繰り返しを避ける
チェーン可能になった Entity クラスですが、各 setter メソッドに多少改善したい点があります。
public Entity id(int id) {
this.id = id;
return this;
}
public Entity name(String name) {
this.name = name;
return this;
}
public Entity value(String value) {
this.value = value;
return this;
}
今回の Entity クラスは単純なものなので、もともと各 setter は引数の値を private フィールドに格納する 1 行だけのシンプルなものでした。しかし、メソッドの型を void から Entity に変えてチェーン可能にするために、すべての setter で最後に return this; することが必須になってしまいます。
次のようにすることで、これを改善できます。
public Entity id(int id) {
return self(this.id = id);
}
public Entity name(String name) {
return self(this.name = name);
}
public Entity value(String value) {
return self(this.value = value);
}
private Entity self(Object... ignored) {
return this;
}
return this; するだけの private メソッド self() を導入し、各 setter からはこのメソッドを呼び出します。return this; が return self(); に変わるだけでは無意味ですが、可変長引数の構文を使用して self() メソッドが任意個数の Object を受け取れるようにしておくことで、各 setter で引数の値をフィールドに代入する式 this.xxx = xxx を self() メソッドの引数として渡すことができます。self() メソッド内では引数の値を使用しないので、副作用はなく、単に無視されます。
インタフェースに self() メソッドを抽出する
プロジェクトの多くのクラスをチェーン可能にする場合は、前のセクションで書いた self() メソッドをインタフェースに抽出したくなります。
これは、Java のジェネリクス(総称型)とインタフェースの default メソッドを使用して、次のように実現できます。
public interface Chainable<T> {
@SuppressWarnings("unchecked")
default T self(Object... ignored) {
return (T) this;
}
}
public class Entity implements Chainable<Entity> {
public Entity id(int id) {
return self(this.id = id);
}
...
}
self() メソッドはジェネリック インタフェース Chainable<T> に移動し、default メソッドとして実装を記述します。型引数 T を使用して self() メソッドの戻り値の型を宣言し、this を T にキャストして返します。
未検査のキャストを行っているため、警告を抑制するには @SuppressWarnings("unchecked") アノテーションが必要です。
チェーン可能にしたいクラスでは、この Chainable<T> インタフェースを実装し、self() メソッドを使用して setter を実装します。
public class SomeModel implements Chainable<SomeModel> {
public SomeModel foo(Object foo) {
return self(this.foo = foo);
}
...
}
1 つ注意点があります。
最初に Entity クラスに追加した self() は、クラス外に公開する必要のない実装の詳細であるため、private メソッドでした。しかし Chaintable<T> に抽出した self() は仕様上、public メソッドになります。現在の Java ではインタフェースの default メソッドを protected にすることはできないので、self() メソッドは Chainable<T> の外から常にアクセス可能です。
SomeModel some = new SomeModel();
// 呼べてしまう
//
some.self();
これは、クラスの設計としては少しおかしい状態だといえます。今のところこれ以上によい実装を思いつかないので、私は気にしないことにしています。
(補足: インタフェースではなくベースクラスに protected メソッドとして self() を置けばよいと思うかもしれません。これが適用できるケースも確かにあります(プロジェクト内で、一部のクラス階層でだけチェーン可能なメソッドを使用したい場合)が、たいていはうまくいかないと思います。Java ではベースクラスは単一であり、チェーン可能なメソッドを提供したいクラスのベースクラスをいつでも自由に選択できるとは限らないためです)
まとめ
チェーン可能な setter を持つ Java クラスは、メソッドの戻り値としてオブジェクト自身を返すことで、複数のメソッドを次々に連結して(メソッドチェーンを形作って)呼び出すことができます。Java 標準ライブラリの中でも、java.lang.StringBuilder クラスや java.util.Calendar.Builder クラスのように多くの使用例があります。一連の設定に基づいてオブジェクトをビルドする builder クラスでよく見られるパターンですが、それ以外の通常のクラスでも活用すると便利なスタイルだと思います。
値を設定するだけのシンプルな setter に限らず、戻り値の型を void にしてメソッドを定義しようとするときには、少し手を止め、オブジェクトクラス自身を返すことでクライアントコードをもっと簡潔に書けるようになる余地がないか、考えてみるのもよいのではないでしょうか。