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