はじめに
並行継承階層(Parallel Inheritance Hierarchies) とは、
あるクラス階層を拡張するとき、必ず別のクラス階層にも同じように新しいサブクラスを作らなければならない状態 を指します。
例:
-
ShapeにRectangleを追加したら、必ずShapeRendererにもRectangleRendererを追加しないといけない -
Documentに新しいサブタイプを追加すると、DocumentParserにも対応するサブクラスを追加しなければならない
これは 継承の構造が強く結びついている ために発生するコードの悪臭です。
18.1 特徴
- 片方の継承階層にクラスを追加すると、必ずもう片方にもクラスを追加 しなければならない
- 実装が 二重管理 になり、変更コストが増大
- 「A の階層」と「B の階層」が 常に並行して成長 してしまう
18.2 解決手法
-
継承から委譲へ(Replace Inheritance with Delegation)
→ 階層を作らず、オブジェクトに処理を委譲する -
共通インターフェースの導入
→ 共通の抽象を定義し、2つの階層を切り離す -
ダブルディスパッチ/Visitor パターン
→ 並行階層の分岐をまとめて扱う -
戦略パターン(Strategy Pattern)
→ 動作部分を戦略として分離し、継承ではなく組み合わせで対応
18.3 Kotlin 例
Before:並行継承階層
// 図形ごとにレンダラーも並行して追加する必要がある
open class Shape
class Circle : Shape()
class Rectangle : Shape()
open class ShapeRenderer
class CircleRenderer : ShapeRenderer()
class RectangleRenderer : ShapeRenderer()
-
Circleを追加したらCircleRendererも追加しなければならない - 変更が「並行」して増える → Shotgun Surgery に近い構造
After①:委譲で解消
interface Renderer {
fun render()
}
abstract class Shape(protected val renderer: Renderer) {
abstract fun draw()
}
class Circle(renderer: Renderer) : Shape(renderer) {
override fun draw() = renderer.render()
}
class Rectangle(renderer: Renderer) : Shape(renderer) {
override fun draw() = renderer.render()
}
→ 描画処理を Renderer に委譲。新しい Shape を追加しても Renderer 側は変更不要。
After②:Visitor パターン
interface ShapeVisitor {
fun visit(circle: Circle)
fun visit(rectangle: Rectangle)
}
abstract class Shape {
abstract fun accept(visitor: ShapeVisitor)
}
class Circle : Shape() {
override fun accept(visitor: ShapeVisitor) = visitor.visit(this)
}
class Rectangle : Shape() {
override fun accept(visitor: ShapeVisitor) = visitor.visit(this)
}
class RenderVisitor : ShapeVisitor {
override fun visit(circle: Circle) = println("Render Circle")
override fun visit(rectangle: Rectangle) = println("Render Rectangle")
}
→ Shape 階層と Renderer 階層を分離し、Visitor で拡張。
18.4 実務での指針
- 並行して増えるクラス階層は 設計の匂い
- 「is-a」ではなく「has-a」や「can-do」で表現できないか再検討
- Strategy / Visitor / Delegation を使うことで、階層の爆発を防ぐ
- Kotlin の場合は sealed class + when で網羅性チェックを利用するのも有効
まとめ
- Parallel Inheritance Hierarchies は「二重管理」が問題の根源
- 解決には 委譲 / Strategy / Visitor を活用
- 基本思想:継承は増殖を招きやすい。委譲や抽象化で切り離して管理する