ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

継承を使うな

一応本記事はC#を想定して書いている。が、オブジェクト指向型の、継承が言語機能として用意されている言語なら同じことが言える。

継承

継承を使う際にはリスコフの置換原則を守るように言われる。

リスコフの置換原則とは

S が T の派生型であれば、プログラム内で T 型のオブジェクトが使われている箇所は全て S 型のオブジェクトで置換可能

になるようにしろという原則である。
ざっくり言うと、基底クラスが見越してないものを派生クラスで書くなという感じだが、詳細は書かない。今はそういう話をしたいわけではない。 一応参考を載せておくが、本記事の本題ではないことは改めて言っておく。

qiita.com

リスコフの置換原則だけでは語れない

リスコフの置換原則は勿論大事だ。守らないという例外は無い。だが、リスコフの置換原則を満たしているか? くらいであればよほど酷いプログラマでなければ判断できる。抽象化したいくせに派生クラスでpublicなメンバを軽率に定義してしまうようなどうしようもないやつはそれほど多くはない。だが僕の経験上、不適切な継承のせいで設計から柔軟性が失われており、改修に困るケースは非常に多い。なぜか。簡単な話だ。

仕様変更の結果、不適切となったから

だ。
最初はis aだったのだろう。同じ基底クラスを持つ様々な派生クラスもis aだったのだと思う。が、仕様変更によってそうではなくなったのだ。特定の派生クラスにだけpublicなメソッドを追加したくなった。引数を増やす必要が出た。などなど。こうなったらもう同じ基底クラスを継承しているのは不適切である。無駄な引数、型による分岐、asキャストが横行し、メソッドの処理など読めるはずもなく、地獄と化す。

継承を使うな

仕様変更によって継承関係が不適切になることがある。且つ、仕様変更は全て予測できるようなものではない。このことからわかるのは、継承は仕様変更されうるようなところで使うべきではないということだ。
行うべきは継承より委譲、である。基底クラスに持たせたかった機能を別のクラスにし(たくさん機能があるなら勿論責務ごとにクラスを作る)、基底クラスを継承するのではなく、インターフェースを実装するのである。これが答えだ。勿論、インターフェース分離の原則も忘れてはならない。

継承の使い所は無いわけではない

では継承など要らないのではないか。正直個人的な意見としてはそうだ。要らない。実際GoやRustには継承は無く、もっと抽象的な、インターフェースのようなものだけが用意されている。しかし、「インターフェース + 責務を負った具象クラス」よりも「具体的処理を持った抽象クラス」の方が有利な場面が存在する。それは「低レイヤのライブラリ」である。

低レイヤのライブラリ

継承はゴミだ、というようなことを言ってきたが、フレームワークなどでは継承されることを前提としたクラスなど珍しくはない。そして、それで困らないということも多いと思う。これはアプリケーションの仕様とは無関係な所、「どのように実装するか」というだけの部分で使われているからである。UnityのMonoBehaviourやUnrealEngineのActorなどは良い例だ。あれらはエンジン側は基底クラスとして扱うが、アプリケーション開発者はそんな抽象化を考えていない。SerializeFieldなどを使って、当然のように派生クラスのインスタンスとして扱っているはずだ。抽象化などハナから気にしていないのだから、この使用方法において、委譲も継承も違いはない。だから困らない。

これだけでは、継承の方が「有利」な説明にはならない。なので次に「有利」である理由を説明する。

インターフェースよりも抽象クラスが有利なただ1つの側面

「インターフェース + 責務を負った具象クラス」と「具体的処理を持った抽象クラス」の違いを考える。
前者の場合、使用者はインターフェースと具象クラスの2つの型を意識しなければならないのに対し、後者は意識すべき型は抽象クラス1つになる。知るべき知識は少ない方がいい。なのでこの点においては後者が有利だ。名著、かどうかはわからないが割とおススメされがちな本、「.NETのクラスライブラリ設計」ではインターフェースよりも抽象クラスを勧めている。改訂版において、UniRxやUniTaskの作者である河合氏から、「インターフェースの方がいいだろ」と至極真っ当なツッコミが入っているのだが、彼らが「ライブラリ提供者」の立場で喋っていることを考えると得心がいく。彼らは、抽象化など考えないケースでは抽象クラスが有利だと言っていたのだ(と僕は解釈した)。

www.amazon.co.jp

結論

継承は使うな。少なくとも使う「必要」は絶対に無い。難しいことがわからないなら「使わない」で良いと思う。いや、MonoBehaviourなどを継承するのは良い。「使わない」とは「自作クラスを継承しない」だと解釈してほしい。C#なら、自作クラスの全てにsealedを付けよう。
だが、低レイヤの、つまりアプリケーションの仕様とは全く無関係な汎用的なライブラリならばその限りではない。

余談1: 抽象クラスという名前の悪質さ

抽象クラスという名前は極めて悪質であるように思う。ここまで読んでくれたなら、そして、継承に苦しめられた人ならわかってくれると思うのだが、抽象クラスは往々にして具象を含むのだ。
本当に完全に抽象的ならそれはインターフェースだ。わざわざ抽象クラスを使うなら、そのプログラマは具象的なメンバを持たせたいと思っているはずだ。そしてその具象が負債となる。抽象クラスは抽象化をしていないとは言わないが、抽象化に成功はしていない。この名前の不一致は、安易に継承を使う者がいつまでも減らない一因になっているのではないかと思う。

余談2: ミノ駆動氏作「共通化の罠」

似た話として、ミノ駆動氏が作成した共通化の罠という動画がある。これは共通化の粒度、つまり凝集度が間違っているという話である。こちらの場合、共通化したモジュールを後から小分けにすれば一応復活できる。決して簡単ではないが、継承が使われているよりはまだ傷は浅い。

qiita.com