「継承」と「委譲」の違い

この記事では「継承」と「委譲」の違いを次のように定義した。

  • 継承 ... 継承前の機能を全て使いたい(使って違和感がない)
  • 委譲 ... 機能の一部分を使いたい

継承の例でよくでるAnimalクラス。抽象的すぎ?

で、思った。継承するのが前提なら、継承の例でよくあるAnimalクラスって抽象的すぎて何でもできるから、クラス肥大化するよね?ってこと。

// 動物 ベースクラス 
class Animal {

    // 動物に関してどんなメソッドでも書けてしまう

    // 鳴く
    bark() { }

    // 食べる
    eat() { }

    // 卵を産む
    layEggs() { }

    // 子供を産む
    bearChildren() { }
}

SOLID原則に当てはめると、SLIの原則から外れたクラスになる。

  • 単一責任の原則 (single-responsibility principle)
  • リスコフの置換原則 (Liskov substitution principle)
  • インターフェース分離の原則 (Interface segregation principle)

たとえば、上のAnimalクラスを継承してDog(犬)Cat(猫)クラスを作成する。でも現実世界では、卵を産む犬も猫も存在しない。

また、Animalクラスでは抽象度が高く、動物に関連していればどんなメソッドでも書けてしまうためクラスが肥大化する。結果、保守できなくなる。

継承するなら1段階下の具体的なオブジェクトへ

そこで、継承するなら1段階下の具体的なオブジェクトにすれば?という感じ。たとえば、鳥や哺乳類など。

  • Animal(動物) → Bird(鳥)
  • Animal(動物) → Insect(昆虫)
  • Animal(動物) → Mammal(哺乳類)

こうすれば、Mammal(哺乳類)ベースクラスを作成しlayEggs()メソッドを入れなければいい。

よく作ってしまいがちなUserクラスでも同じことが言える。どんなユーザがいるのか?InHouseUser(社内ユーザ)OutsideUser(外部ユーザ)AdminUser(管理者ユーザ)など。

「○○は、××する。」が成立すればいい

継承で気をつけるのは継承後クラス、メソッドに対して「○○は、××する。」「継承後クラスは、全てのメソッド(継承前含め)することができる。」が成り立てばいい。

たとえば、次のようにBird(鳥)クラスから継承して、Hawk(鳥)クラスを作るとする。

/**
 * 鳥クラス
 * @class Bird
 */
class Bird {

    // 飛ぶ
    fly() { }

    // 卵を産む
    layEggs() { }
}

/**
 * 鳥クラスを継承した鷹
 * @class Hawk
 * @extends {Bird}
 */
class Hawk extends Bird { }

メソッドはflylayEggsがありこのメソッドをHawkクラスに当てはめる。「鷹は(各メソッド)する。」が成り立てばOK。

  • 鷹は飛ぶ
  • 鷹は卵を産む

継承より委譲

委譲って簡単に言えば普段使ってる組み込みオブジェクトと同じ。インスタンス化、特定のメソッドを使う使いかた。

継承より見通しがよく、継承元メソッドを気にすることなく実装できるため単一責任の原則に従い、シンプルに実装し委譲すること

積極的に使うのは委譲でOK

そもそも普段使用している言語で使っている組み込みオブジェクト(JavaScriptならError()など)。通常、積極的に継承しようとはならないだろう。積極的に使うのは委譲でOK。

まとめ

  • 継承元クラスは抽象度を1段階下げたオブジェクトへ
  • 継承後のクラスで「○○は、××する。」がメソッド全て(継承元含め)成り立てばいい
  • 継承は保守しづらくなるため積極的に使うのは委譲でOK

クラスには抽象的ではなく、具体的なオブジェクト名をつけるといい。「責務」と呼ばれる、作成したクラスでどんな処理までさせていいかが明確になるから。

良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方を読むと理解が深まるので一応紹介しておく。

今回はここまで。