🐼

フェイクコンストラクタを使った段階的な拡張

2024/12/08に公開

はじめに

KotlinにはJavaにはない便利な機能がたくさんありますが、そのうちの1つはFake constructors(フェイクコンストラクタ)でしょう。

フェイクコンストラクタは、見た目はクラスのコンストラクタなのですが実体はただのトップレベル関数です。

Kotlinの標準ライブラリにも存在し、例えば次のように定義されています。

public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)

public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableList<T> {
    val list = ArrayList<T>(size)
    repeat(size) { index -> list.add(init(index)) }
    return list
}

上記のコードは次のように呼び出すことができます。

val list: List<String> = List(4) { "User$it" }
println(list) // [User0, User1, User2, User3]

Effective KotlinのConsider factory functions instead of secondary constructorsでは、フェイクコンストラクタの利点として次のようなものが挙げられています。

  • 実装をインターフェースで隠す。
  • 引数に応じて最適な実装を返す。
  • コンストラクタではできないオブジェクト作成にアルゴリズムを活用する。

利用者の視点からすると呼び出しているのがコンストラクタなのか関数か区別がつかないわけですが、区別がないからこそ便利だと言えます。私は、この特徴を踏まえて、自作ライブラリ(モジュール)を段階的に拡張する際にフェイクコンストラクタを利用しています。

この記事では、段階的な拡張の例をステップバイステップで説明していきます。

ステップ0 - 最初の例

Userというクラスの特定メソッドを呼び出す単純な例を考えます。

// クラス
class User(private val name: String) {
    fun say(): String {
        return "Hi, I'm $name!"
    }
}

// 呼び出しコード
fun main() {
    val user = User("Nick")
    println(user.say()) // Hi, I'm Nick!
}

ステップ1 - Userクラスをインタフェースに変更

ステップ0でクラスとして定義したUserですが、利用者が任意の実装を持てるようにインタフェースに変更します。その際、Userクラスのコンストラクタを呼び出しているコードに影響を与えたくありません。ここでフェイクコンストラクタが役立ちます。

// クラスからインタフェースに変更
interface User {
    fun say(): String
}

// フェイクコンストラクタ
fun User(name: String): User {
    return object : User {
        override fun say(): String {
            return "Hi, I'm $name!"
        }
    }
}

// 呼び出しコード:変更なし
fun main() {
    val user = User("Nick")
    println(user.say()) // Hi, I'm Nick!
}

Userクラスをインタフェースに変更したのに呼び出しコードが一切影響を受けないのがポイントです。

ステップ2 - リファクタリング(Userインタフェースの実装クラスの作成)

上記の例では、フェイクコンストラクタの中でUserインタフェースの実装オブジェクトを返していました。シンプルなケースでは問題ないですが、再利用性を考えてクラスとして切り出してみます。クラスの存在は利用者に意識させたくないのでprivate修飾子をつけておきます。

// インタフェース:変更なし
interface User {
    fun say(): String
}

// フェイクコンストラクタ:クラスのコンストラクタを呼び出すように変更
fun User(name: String): User {
    return DefaultUser(name)
}

// Userインタフェースの実装クラス:新規作成
private class DefaultUser(private val name: String) : User {
    override fun say(): String {
        return "Hi, I'm $name!"
    }
}

// 呼び出しコード:変更なし
fun main() {
    val user = User("Nick")
    println(user.say()) // Hi, I'm Nick!
}

当然ながら呼び出しコードは影響を受けません。

ステップ3 - フェイクコンストラクタで条件分岐

フェイクコンストラクタはただの関数ですから、関数内で条件分岐して任意の実装を返せます。ここではパラメータを追加し、パラメータによってUserインタフェースの実装を切り替えたいと思います(パラメータ追加の代わりにフェイクコンストラクタのオーバーロード関数を用意してもいいでしょう)。

// インタフェース:変更なし
interface User {
    fun say(): String
}

// フェイクコンストラクタ:パラメータに応じてUserインタフェースの実装を変更
fun User(name: String, logging: Boolean = false): User {
    val user = DefaultUser(name)
    return if (logging) LoggingUser(user) else user
}

// 変更なし
private class DefaultUser(private val name: String) : User {
    override fun say(): String {
        return "Hi, I'm $name!"
    }
}

// 新規追加:メソッドが呼ばれた処理を委譲しログ出力をするクラス
private class LoggingUser(private val user: User) : User {
    override fun say(): String {
        val message = user.say()
        println("Logging: $message")
        return message
    }
}

// 呼び出しコード:後半部分を追加(前半部分は変更なし)
fun main() {
    val user = User("Nick")
    println(user.say()) // Hi, I'm Nick!

    val user2 = User("Nick", true)
    println(user2.say()) // Logging: Hi, I'm Nick! Hi, I'm Nick!
}

呼び出しコードは、既存部分について影響を受けることなく新しい機能を利用できるようになります。

まとめ

フェイクコンストラクタを使ってモジュールを段階的に拡張する例を示しました。

フェイクコンストラクタを使うと、初期段階ではクラス中心で開発し、APIが安定してきたり柔軟性が必要になってきたりしたら徐々にインタフェースを導入していく、という戦略が取りやすくなります。

逆にいうと、いずれ必要になるかもしれないからという理由でとりあえずインタフェースを作りスピード感を失ってしまう事態を防ぐことができます。

とはいえ、一点注意点があります。クラスのコンストラクタ呼び出しをフェイクコンストラクタに置き換える場合、ソースコードの互換性は保たれますがバイナリの互換性は保たれません。

絶対必須の機能というわけではないですが、状況によっては十分便利だと思いますので、フェイクコンストラクタの利用をぜひ検討ください。

Discussion