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?

【リファクタリング】Pull Up Constructor Body(コンストラクタ本体の引き上げ)

Posted at

1. 概要(Overview)

Pull Up Constructor Body は、サブクラスのコンストラクタで重複して行っている初期化処理(検証・正規化・デフォルト設定など)を
スーパークラスのコンストラクタ(または init ブロック)へ移動して共通化するリファクタリングです。

目的

  • 生成時の共通ロジックを一箇所に集約し、重複を排除
  • 不変条件(invariants)を上位で担保して設計を明確化
  • 生成経路の追加・変更を低コスト化

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

  • 複数のサブクラスが 同じバリデーション/正規化/初期値設定 をしている
  • 生成時の不変条件をどのサブクラスでも守りたい
  • 仕様変更のたびに各サブクラスのコンストラクタを横並び修正している

よくある匂い:

  • Duplicate Code(重複コード)
  • Shotgun Surgery(散弾銃のような修正)
  • Parallel Inheritance Hierarchies(並行継承階層)

3. 手順(Mechanics / Steps)

  1. サブクラスのコンストラクタ本体を比較し、共通初期化を特定
  2. スーパークラスの コンストラクタ引数プロパティinit ブロックを設計
  3. 共通処理を上位へ移動し、サブクラスは super(...) へ委譲(Kotlin は主コンストラクタ呼び出し)
  4. 残る差分は
    • プロパティ初期化の引数化(上位に渡す)
    • Template Method(フック)や Factory へ分離(※注意点参照)
  5. 既存の生成呼び出しをコンパイル修正し、テストで回帰確認

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 Methodprotected 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 と併用

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?