よく使うJavaライブラリで味わうデザインパターン - Strategyパターン


普段よく使うJavaライブラリにも、GoFのデザインパターンが隠されています。日々の作業が忙しく見逃しがちですが、たまにはじっくり一種の芸術ともいえる美しい設計を味わってみましょう。

今回の芸術

ソースファイル

Main.java
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);
    }
}
MyFieldNamingStrategy.java
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;
    }
}
ExampleBean.java
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のキー名)を決めるロジックとして、FieldNamingStrategyGsonBuilderにセットする設計になっています。前回の記事で書いた「Template Methodパターン」では、機能をカスタマイズするために親クラスを継承する仕組みになっていたこと対して、今回はクラスのインスタンスをセットするようになっています。機能のカスタマイズにおいて、継承をできるだけ使わないことに設計の美学がありそうです。一緒に探っていきましょう。

Strategyパターンを使わない場合

冒頭のコードではStrategyパターンが使われていますが、まずはStrategyパターンを使わない場合どのような設計になるのか、Gsonライブラリの設計者になった気持ちで考えてみましょう。

やりたいことは、フィールド名に"_"が付いているクラスをJSONに変換する際に、"_"を付けないようにすることです。なお、変換対象のクラスはたくさんあること、また、クラスをリファクタリングしてフィールド名を変更することはできないという前提です。

継承を使った設計

オブジェクト指向プログラミングにおいて最初に語られるのはクラスの継承ですので、継承を使った設計を試してみましょう。フィールド名を決めるロジックを、Gsonクラスを継承して実装してみます1

MyGson.java
public class MyGson extends Gson {
    @Override
    protected String translateFieldName(Field field) {
        // フィールド名の先頭に"_"が付いていたら除去した文字列を返す。
        // "_"が付いていなければそのまま返す。
    }
}
Main.java
...
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を見ると、フィールド名以外にも、たくさんのカスタマイズできるポイントがあります。

  1. setExclusionStrategy - JSONに含めないクラスやフィールドを定義
  2. setLongSerializationPolicy - Long型を数値にするか数字にするかを定義
  3. setDateFormat - Date型を文字列にする際のフォーマットを定義

などです。

これらのカスタマイズを、すべて親クラスの継承で行うと、困ったことが起きてきます。それは、機能の組み合わせが爆発してしまうということです。

たとえば、上の1の機能をオーバーライドしているGson1、2の機能をオーバーライドしているGson2、3の機能オーバーライドしているGson3のクラスを使っているとします。

1の機能 2の機能 3の機能
Gson1クラス
Gson2クラス
Gson3クラス

この状況で、新しく1と2の両方の機能が必要になったとき、両方のメソッドをオーバーライドしたGson12クラスが必要になってきます。メソッドの中身はGson1Gson2それぞれで記述されているものとまったく同じなので、ソースコードは重複します。

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パターンの流れは以下のようになります。

  1. GsonBuilderにより、GsonMyFieldNamingStrategyがセットされる
  2. Gson#toJson内でフィールド名を変換する際に、MyFieldNamingStrategy#translateNameが呼ばれる

ポイントは、フィールド名を変換する処理をGsonクラスで直接行うのではなく、MyFieldNamingStrategyに委譲していることです。このようにすることで、メインとなるクラス(Gson)と、変換ロジックのクラス(XXXStrategy)を分離させることができます。分離させることで、ロジックをごっそり取り替えたり、再利用したりすることが簡単にできるようになります。美しいですね!

StrategyパターンのGoFでの定義は「アルゴリズムのファミリを定義し、それぞれをカプセル化し、それらを交換可能なものにすること。Strategyパターンにより、クライアントが使用するアルゴリズムを、独立して変更できるようになる2」です。ここでの「アルゴリズムのファミリ」は、フィールド名を変換するなどの機能(XXXStrategyクラス)に相当します。

Strategyパターンへの専門家のコメント

多くの専門家からも、Strategyパターンを評価するコメントが寄せられています。

結城浩さん

Strategyパターンでは、そのアルゴリズムを実装した部分がごっそりと交換できるようになっています。アルゴリズム(戦略・作戦・方策)をカチッと切り替え、同じ問題を別の方法で解くのを容易にするパターン、それがStrategyパターンなのです。

『Java言語で学ぶデザインパターン入門』 より

lang_and_engineさん

ある程度複雑なアルゴリズムになると,ふつうのプログラマであれば,その部分を切り出して集約するだろう。また,他のアルゴリズムに置き変わった時に備えて,互換性も持たせておくだろう。

GoFの23のデザインパターンを,Javaで活用するための一覧表 より

最後に

わざわざ美術館に行かなくても、たった数行のコードを眺めるだけで知的な愉しみを味わうことができるのは、プログラマーの醍醐味でしょう。

Strategyパターンの芸術性に共感してくださったエンジニアの方は、ぜひ当社(クオリサイトテクノロジーズ株式会社)の採用担当までご連絡ください!

補足(GsonのTips)

上記の例では、Gsonでフィールド名を変換するためにFieldNamingStrategyクラスを使いましたが、変換対象クラスのメンバ変数にアノテーションをつけるだけでJSONでの名前を決めることもできます。変換対象のクラスが少ないときに便利です。

ExampleBean.java
public class ExampleBean {
    @SerializedName("firstField")
    private String _firstField = "value";

    @SerializedName("secondField")
    private String _secondField = "vaule";
}

参考URL

関連記事

インスタンスを作る

インターフェイスをシンプルにする

他のクラスに任せる


  1. Gsonクラスはfinal宣言されているため、実際には継承することができません。ここでは、自分がGsonの設計者だとして、どのような設計にすればよいかを検討している段階だと仮定しています。 

  2. 『オブジェクト指向における再利用のためのデザインパターン』 より