1. 概要(Overview)
Pull Up Constructor Body は、サブクラスのコンストラクタで重複して行っている初期化処理(検証・正規化・デフォルト設定など)を
スーパークラスのコンストラクタ(または init ブロック)へ移動して共通化するリファクタリングです。
目的:
- 生成時の共通ロジックを一箇所に集約し、重複を排除
- 不変条件(invariants)を上位で担保して設計を明確化
- 生成経路の追加・変更を低コスト化
2. 適用シーン(When to Use)
- 複数のサブクラスが 同じバリデーション/正規化/初期値設定 をしている
- 生成時の不変条件をどのサブクラスでも守りたい
- 仕様変更のたびに各サブクラスのコンストラクタを横並び修正している
よくある匂い:
- Duplicate Code(重複コード)
- Shotgun Surgery(散弾銃のような修正)
- Parallel Inheritance Hierarchies(並行継承階層)
3. 手順(Mechanics / Steps)
- サブクラスのコンストラクタ本体を比較し、共通初期化を特定
- スーパークラスの コンストラクタ引数・プロパティ・
initブロックを設計 - 共通処理を上位へ移動し、サブクラスは
super(...)へ委譲(Kotlin は主コンストラクタ呼び出し) - 残る差分は
- プロパティ初期化の引数化(上位に渡す)
- Template Method(フック)や Factory へ分離(※注意点参照)
- 既存の生成呼び出しをコンパイル修正し、テストで回帰確認
4. Kotlin 例(Before → After)
4.1 重複する検証・正規化の引き上げ
Before
class Engineer(val id: Int, name: String) {
val name: String
val createdAt: Instant
init {
require(id > 0) { "id must be positive" }
this.name = name.trim()
this.createdAt = Instant.now()
}
}
class Manager(val id: Int, name: String) {
val name: String
val createdAt: Instant
init {
require(id > 0) { "id must be positive" }
this.name = name.trim()
this.createdAt = Instant.now()
}
}
After
open class Employee(
val id: Int,
rawName: String,
val createdAt: Instant = Instant.now()
) {
val name: String = rawName.trim()
init {
require(id > 0) { "id must be positive" }
}
}
class Engineer(id: Int, name: String) : Employee(id, name)
class Manager(id: Int, name: String) : Employee(id, name)
- バリデーション・正規化・デフォルト設定を
Employeeに集約 - サブクラスは 引数を渡すだけ になり、重複が消える
4.2 差分がある場合:引数化 or Template Method に分離
Before
class CsvReport(filename: String) : Report() {
val path: Path
init {
require(filename.endsWith(".csv"))
path = Paths.get("/data/csv", filename)
}
}
class JsonReport(filename: String) : Report() {
val path: Path
init {
require(filename.endsWith(".json"))
path = Paths.get("/data/json", filename)
}
}
After(共通:検証 + パス生成ルールを上位へ、差分は引数化)
open class Report(
filename: String,
private val expectedExt: String,
baseDir: String
) {
val path: Path
init {
require(filename.endsWith(expectedExt)) { "invalid extension" }
path = Paths.get(baseDir, filename)
}
}
class CsvReport(filename: String) : Report(filename, ".csv", "/data/csv")
class JsonReport(filename: String) : Report(filename, ".json", "/data/json")
もし前後処理の手順自体が異なるなら、上位で共通フローを定める Template Method(
protected open fun buildPath(...))に分離して、サブクラスで差分を実装するのが有効です。
ただし Kotlin ではコンストラクタ中にopenメソッドを呼ぶ設計は非推奨(未初期化参照の危険)なので、**コンストラクタ外(Factory など)**へ移すのが安全です。
4.3 secondary constructor の共通化(主コンストラクタへ集約)
Before
class User {
val id: Long
val name: String
constructor(id: Long, name: String) {
require(id > 0)
this.id = id
this.name = name.trim()
}
constructor(name: String) : this(Random.nextLong(1, Long.MAX_VALUE), name)
}
After(主コンストラクタ + init へ集約)
class User(val id: Long, rawName: String) {
val name: String = rawName.trim()
init { require(id > 0) }
constructor(name: String) : this(Random.nextLong(1, Long.MAX_VALUE), name)
}
- 2つのセカンダリコンストラクタに散在していた検証/正規化を主コンストラクタへ集約
5. 効果(Benefits)
- 重複排除:生成ロジックが一箇所にまとまり、変更が楽
- 不変条件の一元化:どのサブクラス経由でも常に同じルールが適用
- 可読性/安全性向上:サブクラスのコンストラクタが軽量化
6. 注意点(Pitfalls)
-
openメソッドをコンストラクタから呼ばない:未初期化状態での動的ディスパッチは危険- 差分フックが必要なら Factory/Builder での構築に寄せる
- 上位に寄せすぎて God Class 化 しないように注意
- 例外スロー(
require/check)の責務境界を見直す(上位の失敗が呼び出し側に伝播する設計に整合しているか) - 依存注入(DI)と相性を確認:上位へ引き上げたことで コンストラクタ引数 が増えすぎたら、Parameter Object/Factory で整理
まとめ
-
Pull Up Constructor Body は、サブクラスに散在する生成時の共通処理を上位へ集約し、
不変条件の一元化と重複排除で保守性を高めるリファクタリング。 - 判断基準:その初期化はすべてのサブクラスに必要か?/引数化で共通化できるか?
- 設計の指針:
- 単純な共通化 → 上位の主コンストラクタ +
init - 手順差分あり → Template Method(ただし ctor 呼び出しは避ける) or Factory/Builder
- 引数が膨らむ → Introduce Parameter Object と併用
- 単純な共通化 → 上位の主コンストラクタ +