🦁

Kotlin で最適な Builder パターンについて考えてみた

2021/04/23に公開

Java で書かれた Builder パターンの実装を Kotlin に置き換えたのですが、意外と情報が少ないなと感じたのでこの機会に最適な Builder パターンについて考えてみました。
本記事では Builder パターンの細かい説明はしませんのでご了承ください。

前提

今回の Builder パターンの使い道として、複数のプロパティを持つクラスがあって、コンストラクタで全部指定するのが面倒な場合に導入するケースを考えます。
例えばユーザデータを扱うクラスがあり、情報としては下記を持つものとします。

  • ID
  • 名前
  • メールアドレス
  • パスワード

Java の場合

Effective Java で紹介されている Builder パターンを参考に実装すると下記のような感じになります。

User.java
public class User {
    // 各プロパティを読み取り専用にする
    public final Integer id;
    public final String name;
    public final String email;
    public final String password;

    public static class Builder {
        private Integer id;
        private String name;
        private String email;
        private String password;

        public Builder setId(Integer id) {
            this.id = id;
            return this;
        }

        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        public Builder setEmail(String email) {
            this.email = email;
            return this;
        }

        public Builder setPassword(String password) {
            this.password = password;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }

    // コンストラクタを外部から参照できなくする
    private User(Builder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.email = builder.email;
        this.password = builder.password;
    }
}

ポイントとしては下記 2 点です。

  • 各プロパティを読み取り専用にする
    • 後で書き換えできなくすることで意図しない変更を防ぎます
// 各プロパティを読み取り専用にする
public final Integer id;
public final String name;
public final String email;
public final String password;
  • コンストラクタを外部から参照できなくする
    • 必ず Builder 経由でインスタンスを生成させるようにします
private User(Builder builder) {
    this.id = builder.id;
    this.name = builder.name;
    this.email = builder.email;
    this.password = builder.password;
}

Kotlin の場合

前述したポイントを踏まえて Kotlin で実装すると下記のようになります。

User.kt
data class User private constructor(val id: Int, val name: String, val email: String, val password: String) {

    class Builder() {
        private var id = 0
        private var name = ""
        private var email = ""
        private var password = ""

        fun setId(id: Int): Builder {
            this.id = id
            return this
        }

        fun setName(name: String): Builder {
            this.name = name
            return this
        }

        fun setEmail(email: String): Builder {
            this.email = email
            return this
        }

        fun setPassword(password: String): Builder {
            this.password = password
            return this
        }

        fun build(): User {
            return User(id, name, email, password)
        }
    }
}

この 1 行に「各プロパティを読み取り専用にする」「コンストラクタを外部から参照できなくする」が凝縮されています。
Java との違いとして、User クラスのコンストラクタの引数が Builder でないということです(Builder クラスの各プロパティにアクセスできないため)。

data class User private constructor(val id: Int, val name: String, val email: String, val password: String)

さらに、データクラス化することでログ出力や比較なども簡単に実現できます。

ただし、Private data class constructor is exposed via the generated 'copy' method.という警告が出ます。
ここを許容できるかはケースバイケースです。

デフォルト引数を使う

Kotlin ではデフォルト引数がサポートされているので、Builder パターンよりもシンプルに実装できます。

User.kt
data class User(
    val id: Int = 0,
    val name: String = "",
    val email: String = "",
    val password: String = ""
)

val user = User(
    id = 111,
    email = "aaa@bbb.com"
)

まとめ

Kotlin で最適な Builder パターンについて考えてみました。ポイントとしては、下記 2 点があると考えます。

  • 各プロパティを読み取り専用にする
  • コンストラクタを外部から参照できなくする

Java で書く場合と Kotlin で書く場合とでは Kotlin のほうがやはりスッキリ書けました。
また、同じアクセス修飾子でも言語で挙動が異なるという点も確認できました。
もっとより良い Builder クラスの書き方がありましたら意見ください。

追記

デフォルト引数を利用することで、Builder パターンよりもシンプルに実装できるので、こちらがベストなように感じました。

参考ページ

Discussion