普段よく使うJavaライブラリにも、GoFのデザインパターンが隠されています。日々の作業が忙しく見逃しがちですが、たまにはじっくり一種の芸術ともいえる美しい設計を味わってみましょう。
今回の芸術
ソースファイル
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class Main {
public static void main(String[] args) {
// GSONのインスタンスを作成
Gson gson = new GsonBuilder()
.setFieldNamingStrategy(new MyFieldNamingStrategy())
.create();
// JavaのオブジェクトをJSON文字列に変換
String json = gson.toJson(new ExampleBean());
// 出力
System.out.println(json);
}
}
import com.google.gson.FieldNamingPolicy;
import com.google.gson.FieldNamingStrategy;
import java.lang.reflect.Field;
public class MyFieldNamingStrategy implements FieldNamingStrategy {
@Override
public String translateName(Field field) {
// メンバ変数の"_"を取り除く
String fieldName = FieldNamingPolicy.IDENTITY.translateName(field);
if (fieldName.startsWith("_")) {
fieldName = fieldName.substring(1);
}
return fieldName;
}
}
public class ExampleBean {
// メンバ変数の先頭に"_"が付いている
private String _firstField = "value";
private String _secondField = "vaule";
}
実行結果
// メンバ変数の先頭の"_"が取れている
{"firstField":"value","secondField":"vaule"}
Gsonライブラリを使って、JavaオブジェクトをJSON文字列に変換するシーンです。あらためて鑑賞してみると、シンプルさとカスタマイズ性を両立させていそうなインターフェイスに胸が高まります。
鑑賞のポイント
Gsonは、Javaオブジェクト <=> JSON を相互変換する、Google製のオープンソースライブラリです。このGsonライブラリを使って、ExampleBean
クラスをJSONに変換しています。
ExampleBean
クラスは、C++やObjective-Cのプログラマーによって書かれたのでしょう。Javaでは珍しく、メンバ変数の先頭に"_"(アンダースコア)が付けられています。しかし、今回生成するJSONには、"_"は付けたくありません。
Gsonでは、JavaオブジェクトをJSONに変換する際にフィールド名(JSONのキー名)を決めるロジックとして、FieldNamingStrategy
をGsonBuilder
にセットする設計になっています。前回の記事で書いた「Template Methodパターン」では、機能をカスタマイズするために親クラスを継承する仕組みになっていたこと対して、今回はクラスのインスタンスをセットするようになっています。機能のカスタマイズにおいて、継承をできるだけ使わないことに設計の美学がありそうです。一緒に探っていきましょう。
Strategyパターンを使わない場合
冒頭のコードではStrategyパターンが使われていますが、まずはStrategyパターンを使わない場合どのような設計になるのか、Gsonライブラリの設計者になった気持ちで考えてみましょう。
やりたいことは、フィールド名に"_"が付いているクラスをJSONに変換する際に、"_"を付けないようにすることです。なお、変換対象のクラスはたくさんあること、また、クラスをリファクタリングしてフィールド名を変更することはできないという前提です。
継承を使った設計
オブジェクト指向プログラミングにおいて最初に語られるのはクラスの継承ですので、継承を使った設計を試してみましょう。フィールド名を決めるロジックを、Gson
クラスを継承して実装してみます1。
public class MyGson extends Gson {
@Override
protected String translateFieldName(Field field) {
// フィールド名の先頭に"_"が付いていたら除去した文字列を返す。
// "_"が付いていなければそのまま返す。
}
}
...
public class Main {
public static void main(String[] args) {
// GSONのインスタンスを作成
Gson gson = new MyGson();
// JavaのオブジェクトをJSON文字列に変換
String json = gson.toJson(new ExampleBean());
// 出力
System.out.println(json);
}
}
思いのほかスッキリしています…。MyGson
クラスでは、親クラスのtranslateFieldName
メソッドをオーバーライドして、フィールド名を変換するロジックを実装しています。オーバーライドしたメソッドは、親クラスであるGson
クラスから、JSON文字列作成時に呼び出されます。この方式は前回の記事で書いた「Template Methodパターン」ですね。想像以上にシンプルでわかりやすくなりました。
継承を使った設計の問題点
この状況下では、「Template Methodパターンの圧勝だ。Strategyパターンなんていらなかったんだ」と思われても仕方ありません。
しかし、Gsonでカスタマイズできるものはフィールド名だけではありません。GsonのJavadocを見ると、フィールド名以外にも、たくさんのカスタマイズできるポイントがあります。
- setExclusionStrategy - JSONに含めないクラスやフィールドを定義
- setLongSerializationPolicy - Long型を数値にするか数字にするかを定義
- setDateFormat - Date型を文字列にする際のフォーマットを定義
などです。
これらのカスタマイズを、すべて親クラスの継承で行うと、困ったことが起きてきます。それは、機能の組み合わせが爆発してしまうということです。
たとえば、上の1の機能をオーバーライドしているGson1
、2の機能をオーバーライドしているGson2
、3の機能オーバーライドしているGson3
のクラスを使っているとします。
1の機能 | 2の機能 | 3の機能 | |
---|---|---|---|
Gson1クラス | ○ | ||
Gson2クラス | ○ | ||
Gson3クラス | ○ |
この状況で、新しく1と2の両方の機能が必要になったとき、両方のメソッドをオーバーライドしたGson12
クラスが必要になってきます。メソッドの中身はGson1
とGson2
それぞれで記述されているものとまったく同じなので、ソースコードは重複します。
1の機能 | 2の機能 | 3の機能 | |
---|---|---|---|
Gson1クラス | ○ | ||
Gson2クラス | ○ | ||
Gson3クラス | ○ | ||
Gson12クラス | ○ | ○ |
さらに、1と3の機能が必要になれば、同じようにGson13
クラスが必要になり、1, 2, 3の機能が必要になれば、Gson123
クラスを作る必要があります。
1の機能 | 2の機能 | 3の機能 | |
---|---|---|---|
Gson1クラス | ○ | ||
Gson2クラス | ○ | ||
Gson3クラス | ○ | ||
Gson12クラス | ○ | ○ | |
Gson13クラス | ○ | ○ | |
Gson123クラス | ○ | ○ | ○ |
... |
このように、Gson
クラスの継承で解決しようとすると、機能の組み合わせの数だけクラスが必要になる、かつソースコードが重複するという最悪の設計になってしまいます。
なお、継承を使ったもうひとつの方法として、JSONに変換するExampleBean
クラスを継承して、ここでtranslateFieldName
などの各メソッドを実装することも考えられます。しかし、それでも上記と同じ問題に当ってしまいますし、そもそも、Gsonの仕様上の理念である、POJO(普通のJavaオブジェクト)のままでJSONに変換できることに反してしまいます。
Strategyパターンを使った場合
ここでStrategyパターンを適用してみましょう。ソースコード、実行結果は冒頭のものをもう一度ご覧ください。
今回のStrategyパターンの流れは以下のようになります。
-
GsonBuilder
により、Gson
にMyFieldNamingStrategy
がセットされる -
Gson#toJson
内でフィールド名を変換する際に、MyFieldNamingStrategy#translateName
が呼ばれる
ポイントは、フィールド名を変換する処理をGson
クラスで直接行うのではなく、MyFieldNamingStrategy
に委譲していることです。このようにすることで、メインとなるクラス(Gson
)と、変換ロジックのクラス(XXXStrategy
)を分離させることができます。分離させることで、ロジックをごっそり取り替えたり、再利用したりすることが簡単にできるようになります。美しいですね!
StrategyパターンのGoFでの定義は「アルゴリズムのファミリを定義し、それぞれをカプセル化し、それらを交換可能なものにすること。Strategyパターンにより、クライアントが使用するアルゴリズムを、独立して変更できるようになる2」です。ここでの「アルゴリズムのファミリ」は、フィールド名を変換するなどの機能(XXXStrategy
クラス)に相当します。
Strategyパターンへの専門家のコメント
多くの専門家からも、Strategyパターンを評価するコメントが寄せられています。
結城浩さん
Strategyパターンでは、そのアルゴリズムを実装した部分がごっそりと交換できるようになっています。アルゴリズム(戦略・作戦・方策)をカチッと切り替え、同じ問題を別の方法で解くのを容易にするパターン、それがStrategyパターンなのです。
lang_and_engineさん
ある程度複雑なアルゴリズムになると,ふつうのプログラマであれば,その部分を切り出して集約するだろう。また,他のアルゴリズムに置き変わった時に備えて,互換性も持たせておくだろう。
最後に
わざわざ美術館に行かなくても、たった数行のコードを眺めるだけで知的な愉しみを味わうことができるのは、プログラマーの醍醐味でしょう。
Strategyパターンの芸術性に共感してくださったエンジニアの方は、ぜひ当社(クオリサイトテクノロジーズ株式会社)の採用担当までご連絡ください!
補足(GsonのTips)
上記の例では、Gsonでフィールド名を変換するためにFieldNamingStrategy
クラスを使いましたが、変換対象クラスのメンバ変数にアノテーションをつけるだけでJSONでの名前を決めることもできます。変換対象のクラスが少ないときに便利です。
public class ExampleBean {
@SerializedName("firstField")
private String _firstField = "value";
@SerializedName("secondField")
private String _secondField = "vaule";
}
参考URL
- How to prevent Gson serialize / deserialize the first character of a field (underscore)?
- Gsonで@SerializedNameって書くのやめたい
関連記事
インスタンスを作る
- よく使うJavaライブラリで味わうデザインパターン - Factoryパターン
- よく使うJavaライブラリで味わうデザインパターン - Builderパターン
- よく使うJavaライブラリで味わうデザインパターン - Abstract Factoryパターン
インターフェイスをシンプルにする
他のクラスに任せる
- よく使うJavaライブラリで味わうデザインパターン - Template Methodパターン
- よく使うJavaライブラリで味わうデザインパターン - Strategyパターン
-
Gsonクラスはfinal宣言されているため、実際には継承することができません。ここでは、自分がGsonの設計者だとして、どのような設計にすればよいかを検討している段階だと仮定しています。 ↩