オブジェクト指向
ポエム
oop
継承

継承に関する一つのポエム

ポエムと言えば何でも許されると思っているんじゃあない

TL;DR1

  • メソッドの戻り値型や引数型2に継承可能3な具象クラス型4を含めるべきではない(例外は非抽象メソッドの戻り値)
  • 継承元のクラスにオーバーライド可能3な非抽象メソッドを含めるべきではない
  • 書いてて途中で力尽きました

継承とは?

あるクラス5を元に新しいクラスを定義する際に、元のクラスの実装を新しいクラスでも暗黙的に利用できるようにすること、だと思う(だと思う、というのは自信がないからだ)。
継承されるクラスを基底クラス (base class), 継承するクラスを派生クラス (derived class) という(言葉なんかどうでもいい、要はそういう分類ができる、ということだ)。

継承の目的の一つは、あるクラスの中で、特殊な振る舞いをするデータを抽出すること、だと思う(だと思う、というのは自信がないからだ)。
例えば、あるデータの組を一まとめに扱うときに、データの組の構造に対して、クラスを定義することができる。
クラスを定義するにあたって、各要素の型は一様か、各要素の間に順序はあるか(あるとしてどのような順序構造を持つか)、各要素を表現するデータはメモリ上連続か、etc., を考慮したいときがある。それは、それぞれの性質によって、可能な操作が異なるためであり、逆に共通する性質を持つならば、その点に関して実装/インタフェースを共有することができるからだ。
この意識の下では、実装の(計算的な意味でなく、モデリングとしての)最適化が主眼にあるように思える。

継承の効用としては次のようなことが挙げられる(きっとすべてではないだろう):

  • インタフェースの共通化/強制
  • 実装の共通化
  • 多態性6の恩恵の享受

最後の多態性に関しては、メモリ上のデータの配置やコンパイラによる最適化の問題があるらしいが、実際よく知らないし、よほど原始的な環境7でなければそれで困る8ことはないだろうと思うので、この記事ではあまり触れないようにしたい。

継承のつらみ

継承はつらいと言われている。それをよく表す言葉として、次の言葉がある:

継承より委譲を選ぶ(favour composition over inheritance

以下では問題点と言われているものをいくつか挙げる。

派生クラスの振る舞いを制御できない

継承可能なクラスに対して、それが推奨されない行いだとしても、派生クラスが実装されるのは自然の摂理である(おそらくあなたは過去に std::mapstd::vectorpublic 継承したコードを見たことが、あるいは実装した経験があるのではないだろうか?)。

プログラマにとって障害となることの一つは、そのインスタンスの振る舞いが予測不能/予測困難になることである。あるクラスの派生クラスが、基底クラスの実装されたメソッドをオーバーライドしている場合、基底クラスの実装は派生クラスからは(デフォルトでは)呼び出されなくなるので、基底クラスの実装を(誤って)期待してしまった場合に不都合が生じる。この場合、実装を派生クラスか基底クラスのどちらかに寄せるべきであり、それが不都合なら継承しないことを選択すべきである。

基底クラスの修正が困難

一度派生クラスが定義された基底クラスの振る舞いは、原則的に変更することはできない。にもかかわらず、コードの重複を防ぐ試みに、基底クラスを実装して派生クラスから基底クラスへ実装の一部を移動することは、修正の困難さを助長するという意味で、愚かな行いである。もしそのために、単純だが面倒な修正が要求されるようなら、作業を中断して別の問題に取り組むべきではないだろうか9

これについて言えることは、極力基底クラスには実装を書かないでおくべき。

実装の確認が困難

あるクラスのインスタンスが、実際には別の派生クラスのインスタンスであることが実行時にはあり得る。
そこから呼び出されるインスタンスメソッドないしクラスメソッドもまた、実行時には、派生クラスのものが呼び出されることになるはずである。
このことについて、ソースコード上で処理を理解しようとすると、基底クラスの変数に対するメソッド呼び出しから、派生クラスのメソッド定義を確認する必要があり、それは複数のソースコードを跨ぐので、煩雑になりがちである。

この問題に関しては、ソースコードより(それが存在するならば)ドキュメントを読むべきだし、継承に限った問題ではないのであまり掘り下げない(もしかして:問題を誤解している?)。

やりたいことは?

継承(に限らずある種のリッチな言語機能)を利用するにあたり、何を目的とするかは明確であった方がいいと思う。

ダックタイピングの模倣

継承を利用して実現したいことは何だろうか。考えてみて直ぐに思いつくことはダックタイピングの模倣である。
ダックタイピングは、共通する振る舞いを持つものは同一のものと見なす、ある種の型推定である、と思う(と思う、というのは自信がないからだ)。

継承はダックタイピングとは違う機構なので、それそのものを自然に表現できるわけではないが、基本的な考えは次の通り:

  • 小さな(実装を持たない)インタフェースを定義し、大きなインタフェースはそれらを多重継承したものとして定義する
  • メソッドの引数型を(実装を持たない)インタフェース型に限定する

可能な限りインタフェースを細分化することで、「アヒルめいたもの」を定義しやすくする。また、メソッド内で呼び出し可能なメソッドを限定しやすくする。
実装を持つ型をメソッドの引数型に含めてしまうと、その実装されたメソッドに依存するため、コードが脆弱になりがちだと思う。そのために、引数型をインタフェース型に限定することは特に重要だと思われる。

まとまったコードが書けるなら、利便性は上がると思う。

弱点としては次のことが考えられる:

  • そもそもダックタイピングやりたいならそうするし、ジェネリクスの方が向いている
  • 無駄に大量のインタフェース型が定義される(面倒)

感想

考えるだに継承は簡便かつ邪悪な手法だと思う。


  1. too long; didn't read”(長過ぎて読めん)の略。「今北産業」的な用法だと思う。少なくとも2003年に遡る、とあるから既に10年選手だ。 

  2. インタフェース、と書こうとしたが意味が広すぎるので止めた。インタフェースとは、メソッドの型、つまり戻り値と引数のデータ型、あるいは加えて、送出し得る例外の(基底)型、呼び出し元のインスタンスへの副作用の可能性の有無、例外安全性の有無を指すように思う。 

  3. Javaでいうところの final が付いていないもの。 

  4. 非抽象クラスの意。実行時にそれ自身のインスタンスを生成することのできるクラス、だと思う(だと思う、というのは自信がないからだ)。 

  5. クラスとは、ある特定の振る舞いを持つデータの集合を指す、のだと思う(だと思う、というのは自信がないからだ)。 

  6. ポリモーフィズム (polymorphism). 言葉だけなら、ゲームに親しみのある人なら、シェイプシフター能力を表現する言葉として、たとえば呪文として、知っている人も多い、と思う(と思う、というのは自信がないからだ)。多態性とは、あるクラスのインスタンスが、他の複数のクラスのインスタンスとして振る舞うことを指すこと、だと思う(だと思う、というのは自信がないからだ)。 

  7. 原始的な環境、例えば、適切なIDEやエディタがインストールできず、またはインストールに面倒な手続き(e.g., 処理に半日~1週間かかる申請システム)が必要だったり、特定のマシンにしかインストールできず、マシンの利用に面倒な手続き(e.g., マシンのある部屋への入室許可の申請が必要)が必要な環境のことである。往々にして、このような環境において、適切なドキュメントは存在せず、ソースの変更履歴はソースコード上にコメントとして記述され、ドキュメント生成を目的としたコメント群はその方法論(というのもおこがましいものだが)のために、メンテナンスされずに朽ち果てている。また、レビューを介していないコードがコミットされ、コード修正者にコンパイル環境がないなどの理由のために単純なミスが履歴に残り、不快なソースコードが山と作られていく、というようなことが起こる。 

  8. 原始的な環境7で、限られた資源を使って処理を検証しようとすると、多態性は障害となる。必然的にオーバーライドされたメソッドの実装を(目)grep 等によって検索することになるが、多態性が解決されるのは実行時においてであり、調査担当者はおそらく実行環境を持たないので(あるいは適切なテストデータが提供されないので)、より多くの労力を割かねばならなくなる。 

  9. 自分はこれができずに大量の修正を行うことになりました。