飛べない鳥問題
継承の誤用例として「飛べない鳥問題」がしばしば挙げられる(Quora)。原典の議論は「鳥という名前に振る舞いを結びつけたのが誤り」という命名の問題として語られることが多い。これには一理ある。そもそも「ある対象が、飛べるものであれば何であれ fly() を実行できる」というのが達成したいことであり、「飛べるものであれば何であれ」が見出した抽象概念なので、これに「鳥」という名付けを行うのが問題といえる。 ではなぜ「鳥」という名前を当ててしまうのか。設計者が既に持っている分類学のスキーマに、新しく見出した抽象を同化させたくなる心理が働くためである。Piaget 1952 は新情報を既存スキーマに当てはめる過程を同化 (assimilation) と呼び、Rosch 1978 は認知的節約原理のもとで新事例がプロトタイプに寄せて分類されることを述べた。Tversky & Kahneman 1974 の representativeness heuristic も、新しい対象を既知の典型との類似度で判断する傾向として同じ現象を扱う。Mohanani et al. 2020 は、この種のバイアスがソフトウェア工学の設計判断に現れることを体系的に整理した。 業務で見出した "できること" の抽象 (「飛べるもの」) を、既知の分類学の名 (「鳥」) に同化させて理解しようとする。ここで止まるなら誤りは命名だけに留まる。問題は次の段階で起こる。分類名 鳥 を基底クラスに据えると、現実世界で「鳥類は飛ぶ」と結びついている属性や振る舞いが、そのまま基底クラスの契約に流れ込む。命名の誤りが誘因となり、契約設計の誤り (分類としての抽象に "できること" を混ぜ込むこと) が生じる。以降で「本質は命名ではなく契約」と述べるのは、この二段構えの後段を指している。
問題の定式化
ハトやカラスは鳥類であり、鳥類は飛ぶので fly() という振る舞いを持つ、という発想で次のようにモデリングする。
code: (mermaid)
classDiagram
class 鳥 {
fly()
}
class ハト
class カラス
class ペンギン
鳥 <|-- ハト
鳥 <|-- カラス
鳥 <|-- ペンギン
ペンギンを加えた時点で、ペンギン.fly() の実装が存在しない。この破綻は「鳥」という名付けではなく、基底クラス 鳥 が全派生に対して fly() の実装を要求しているという契約設計に起因する。
Liskov の置換原則による破綻
Liskov & Wing 1994 は、派生型は基底型が満たす事前条件・事後条件・不変条件を守らなければならないと定式化した。公開された振る舞いは、呼び出し側から観測可能な事後条件を少なくとも暗黙に持つ。fly() を 鳥 の契約に含めた時点で「呼び出し後に飛行している」という事後条件を避けることは難しい。鳥.fly() がこの事後条件を持つ以上、ペンギン.fly() はそれを満たせず置換原則に違反する。派生クラスで例外を投げる、あるいは空実装にする対応は、基底クラスの事後条件を暗黙に弱めるためクライアントコードの前提を壊す (LSP では派生型が事後条件を弱めることは許されない)。 したがって問題は ペンギン を 鳥 の派生にしたことではなく、fly() を 鳥 の契約に含めたことにある。
継承と合成
Gamma et al. 1994 は "Favor object composition over class inheritance" を原則として掲げた。この原則が指す継承の問題は、実装継承による派生クラスと基底クラスの強結合、基底変更が派生を壊す fragile base class、LSP 違反のリスクであり、分類としての型階層そのものを否定するものではない。 「継承は悪」とする主張の対象は実装継承であり、ここで扱うべき争点もそこに限られる。
分類としての抽象と "できること" としての抽象
「鳥類である」ことは生物学的な分類(taxonomy)であり、同一視のための抽象である。「飛べる」ことは "できること" の契約(capability)であり、置換可能性のための抽象である。両者は独立に設計できる。
同種の警告は [Meyer 1997 Object-Oriented Software Construction 2nd ed. https://bertrandmeyer.com/OOSC2/] の "Using Inheritance Well" 章にある Taxomania 節にもある。Meyer は taxonomy と継承は複雑性を飼いならす手段であって複雑性を持ち込むためのものではないとしたうえで、派生クラスは新機能の導入・継承した feature の再宣言・表明 (assertion) の変更のいずれかを行うべきだとする Taxomania rule を提示した。業務要件なしに現実世界の分類階層をクラス階層として定義すれば、複雑性を増やすだけで設計の助けにならない。裏返せば、業務要件に駆動された分類階層は複雑性の管理に寄与する。本稿が属性ベースの分類を擁護するのはこの範囲においてであり、現実世界の taxonomy を無条件に写し取ることを肯定するものではない。 解決モデル
"できること" としての 飛べる を interface として定義し、分類としての 鳥 を抽象クラスとして別に定義する。
code: (mermaid)
classDiagram
class 飛べる {
<<interface>>
fly()
}
class 鳥 {
species
weightGram
}
class ハト
class カラス
class ペンギン
飛べる <|.. ハト
飛べる <|.. カラス
鳥 <|-- ハト
鳥 <|-- カラス
鳥 <|-- ペンギン
抽象概念の見出し方: 振る舞いベースと属性ベース
抽象概念の見出し方には、大きく分けて振る舞いベースと属性ベースの二通りがある。飛べない鳥問題の混乱は、この二つを同じクラス階層に重ねてしまうことから生じる。
振る舞いベース: ポリモーフィズムで扱うために、共通の振る舞い (fly()) を持つことを示す interface を定義する。名前はその振る舞いができること (飛べる) を示すものにする。ペンギンを含める必要はないし、含めると契約を守れなくなる。
属性ベース: 共通の属性 (種名、体重) を持つ分類として抽象を定義する。ドメインのなかで「鳥類として同一視する」業務要件があるときや、データベースの鳥テーブルにマッピングするクラスが必要なときにこちらを使う。ポリモーフィズムを目的にしないなら、ハトやカラスのような子クラスを作る必要もない。
この二つの区別を曖昧にしたまま継承関係を引くと、振る舞いの契約と分類の属性が一つのクラス階層に混ざり、LSP 違反や fragile base class の温床になる。「継承即悪」という極端な主張は、この区別を整理する代わりに継承そのものを捨てようとする反応であり、本来必要な属性ベースの分類まで巻き込んでしまう。
永続化はドメイン抽象を駆動しない
テーブル駆動で設計を進めると、先に 鳥 テーブルが存在し、そのテーブルに対応する 鳥 クラスをそのままドメインモデルとして使うことになりがちである。ORM の Entity クラスをドメインモデルとして兼用する場合にも同じことが起こる。結果として、分類の抽象を置くかどうかが業務要件ではなくテーブル構造によって決まる。これは永続化の都合でドメインの抽象を決めており、関心の方向が誤っている。
飛べない鳥問題の文脈でいえば、ハト・カラス・ペンギンをすべて 鳥 テーブルに格納する設計を先に選ぶと、対応する 鳥 Entity に全種共通の列 (種名、体重) が集まり、fly() を Entity に生やすかどうかも「そのテーブルに入っている種のうち飛べないものがあるか」というテーブル側の事情で決まりがちになる。本来は capability の抽象 (飛べる) と分類の抽象 (鳥) を別に置く判断が先で、Single Table Inheritance で一テーブルに格納するか、Class Table Inheritance や Concrete Table Inheritance でクラスごとに分けるかはその後の永続化設計である。
分類としての 鳥 を定義する根拠は「鳥類として同一視する業務要件がある」ことに限られる。テーブルが一つで済むからではない。
まとめ
「飛べない鳥問題」の本質は命名ではなく、基底クラスの契約設計と抽象の混同である
抽象概念には振る舞いベース (capability) と属性ベース (taxonomy) の二通りがあり、両者を同じクラス階層に重ねると LSP 違反と fragile base class を生む
「継承は悪」とする主張は実装継承への警告であり、属性ベースの分類階層そのものへの批判ではない
永続化戦略はドメイン抽象の後に選ばれる設計決定である