0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【リファクタリング】Replace Constructor with Factory Method(コンストラクタをファクトリーメソッドに置き換え)

Posted at

1. 概要(Overview)

Replace Constructor with Factory Method は、new / コンストラクタ呼び出しをやめて、
意図の分かる名前付きの生成メソッド(ファクトリー) に置き換えるリファクタリングです。

目的

  • 生成意図を名前で明示(ofFreeTier(), fromJson() など)
  • 複雑な初期化・前処理・検証を生成時に隠蔽
  • 返す型を柔軟化(将来サブタイプを返せる)
  • キャッシュ・プール・シングルトン等の最適化を差し替えやすく

2. 適用シーン(When to Use)

  • コンストラクタの引数が多い/意味が分かりにくい
  • 初期化前に検証・正規化・補完が必要
  • 入力の**バリエーション(ID/JSON/URL 等)**に応じて生成方法が複数ある
  • 将来、別実装(サブクラス)を返したい可能性がある
  • 生成時にキャッシュ再利用インスタンス数管理をしたい

匂い:

  • Long Parameter List / Telescoping Constructor
  • Alternative Classes with Different Interfaces
  • Duplicate Initialization Code

3. 手順(Mechanics / Steps)

  1. 既存コンストラクタの呼び出し箇所を洗い出す
  2. 意図ごとに 名前付きファクトリー を設計(ofXxx, fromXxx, createXxx など)
  3. ファクトリー内へ 検証・前処理・デフォルト補完 を移す
  4. 呼び出し側を コンストラクタ → ファクトリー に置き換える
  5. 互換期間が必要なら、コンストラクタを private にして ファクトリーへ委譲 or @Deprecated で誘導
  6. テスト更新

4. Kotlin 例(Before → After)

4.1 意図が不明なコンストラクタ → 名前付きファクトリー

Before

class Plan(val name: String, val price: Int, val isTrial: Boolean)

val free = Plan("Free", 0, true)
val pro  = Plan("Pro", 1200, false)

After

class Plan private constructor(val name: String, val price: Int, val isTrial: Boolean) {
    companion object {
        fun freeTrial(): Plan = Plan("Free", 0, true)
        fun proMonthly(): Plan = Plan("Pro", 1200, false)
        fun fromName(name: String): Plan =
            when (name.lowercase()) {
                "free" -> freeTrial()
                "pro"  -> proMonthly()
                else   -> error("Unknown plan: $name")
            }
    }
}

val free = Plan.freeTrial()
val pro  = Plan.proMonthly()
  • 呼び出しから意図が一目瞭然
  • 生成ルールを内部に集約できる

4.2 入力に応じてサブタイプを返す

sealed interface ImageFormat {
    val bytes: ByteArray
    fun contentType(): String
}

private class Png(override val bytes: ByteArray): ImageFormat {
    override fun contentType() = "image/png"
}

private class Jpeg(override val bytes: ByteArray): ImageFormat {
    override fun contentType() = "image/jpeg"
}

object ImageFormatFactory {
    fun fromHeader(bytes: ByteArray): ImageFormat =
        if (bytes.take(8).toByteArray().contentEquals(byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A)))
            Png(bytes)
        else
            Jpeg(bytes)
}
  • 呼び出し側は 上位型 を受け取り、内部の実装差は隠蔽

4.3 互換移行(段階的差し替え)

class User private constructor(val id: Int, val name: String) {
    companion object {
        fun of(id: Int, name: String): User {
            require(id > 0) { "id must be positive" }
            return User(id, name.trim())
        }
    }

    @Deprecated("Use User.of(id, name)", ReplaceWith("User.of(id, name)"))
    constructor(id: Int, name: String, @Suppress("UNUSED_PARAMETER") legacy: Boolean) :
        this(id, name)
}
  • 旧コンストラクタは 非推奨 とし、IDE が自動置換を提案

5. 効果(Benefits)

  • 可読性:生成意図が名前で伝わる
  • 安全性:検証・補完を中心化して不変条件を守れる
  • 拡張性:将来サブタイプ/キャッシュを返しても呼び出し側は変更不要
  • テスト容易性:生成ロジックを単体テスト可能

6. 注意点(Pitfalls)

  • ファクトリーが巨大化しやすい → 用途別に Factory の分割 or Builder 導入
  • オーバーロード乱立は逆効果 → 命名規約統一of, from, newXxx, createXxx
  • ライブラリ公開 API では移行期間の互換を用意(@Deprecated + 委譲)
  • DI コンテナ利用時はスコープ/ライフサイクルと整合させる(シングルトン返却等)

7. 命名ヒント(よく使う接頭辞)

  • of(...):すでに正規化済みの値から生成
  • from(...)外部表現(文字列/JSON/ファイル/URL 等)から生成
  • newXxx() / createXxx():単純生成(副作用なし/ありで使い分けるチームも)
  • forXxx(...):使用目的/コンテキストに合わせたプリセット

まとめ

  • Replace Constructor with Factory Method は、生成の意味付け・検証・実装差し替えを可能にし、API をわかりやすく安全にする手法。
  • 判断基準:呼び出しから生成意図が読み取れるか? 検証・前処理を集中させたいか? 将来の差し替え余地が必要か?
  • 先の選択肢:
    • 初期化がさらに複雑 → Builder
    • 実装の切替が多い → Factory Method / Abstract Factory / Strategy

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?