よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

『決定版:Javaの継承と抽象化 ~ 第1部 継承 ~ 』

はじめに

本記事は、「継承」「抽象クラス」「インターフェイス」の各々について、それらの相互作用について学ぶことをテーマにした全4部構成記事の第1部です。

『継承・インターフェイス・抽象クラス』シリーズ

  1. Javaの継承』~型と実装の継承・合成と委譲・多重継承問題~
  2. Javaインターフェイス』~歴史・型の継承・ポリモーフィズム情報隠蔽・多重継承問題へのソリューション~
  3. Javaの抽象クラス』~抽象化とは・抽象クラスとサブクラスの粒度差・具体的な使用例(Template Methodパターン)~
  4. Javaの抽象クラスとインターフェイスの違い』~表・使い分け・組み合わせ爆発問題・抽象骨格実装 ~

以上の構成を予定しています。

  • 基本的な情報
  • 特徴・メリット・デメリット
  • 目的や注意点
  • オブジェクト指向の中での役割

などなど深堀していきます。

それぞれの特徴とメリット・デメリット、トレードオフ。登場した当時の歴史的背景や、何に対してのソリューションなのか?オブジェクト指向の目的である「変更容易性」のある設計に対してどの様な貢献をするのか?

など、統一的かつ網羅的な内容にしようと思っています。

言語が用意しているシステムが、何をしたいのか?何のためにあるのか?どんな背景や文脈の元で登場したのか?歴史の中でどう変化していったのか?

  • 再利用性が高い
  • 独立性の高い
  • 変更に対して影響が少ない
  • 変更の影響が特定しやすお
  • 機能追加・拡張が容易
  • 意図がわかる一意な名付け
  • データ抽象化・情報隠蔽
  • データと処理を一つの固まりとして扱える

ピンポイントな情報を見たい場合は目次を用いて抽出してください。長いです。

前置き

継承については以下の様に明示します。

  • 継承(extends):実装・型(仕様)の継承を指す
    • クラス・抽象クラスでの継承のこと
  • 継承(implements):型(仕様)の継承を指す
  • 継承:概念そのものを指す
    • 親の性質と子の性質が同一であること

本記事では幾つかの目線で同一のものを説明します。

  • 言語の歴史的背景によるJava現行仕様の「正統性」の目線
  • オブジェクト指向の目的である「変更容易性」の目線

大きく分けてこの二つかなと思います。ですが、双方密接にからみあう要因ですので両方の目線から話すこともあれば片方の目線でのみ語ることもあります。そのあたりはなるべく明示したいところですが難しい点もあるので文脈・コンテキストから読み取っていただく必要があるかもしれません。というか、あります。もうしわけないっす。

前提知識

Java における『継承』には2種類の目的があります。

型(仕様)の継承:implements

どのようなメソッドを持っているか、どのように振る舞うかを継承。型・メソッド名・引数と引数の方・戻り値の型、といったシグネチャ

実装(コード)の継承:extends

どのようなデータ構造を使い、どのようなアルゴリズムで処理するかを継承。 仕様を実現するための具体的なメソッド・実装の詳細、コードのことです。

  • 型(仕様)継承は『implements』(インターフェース)
  • 実装継承・型(仕様)継承は『extends』(クラス・抽象クラス)

Javaでは、二種類に継承を分けて、型(仕様)の継承のみ多重継承できるようにしています。これはデータ構造の衝突やクラス階層の複雑化などを回避するのが目的です。また、extends による継承は「型(仕様)の継承」「実装の継承」両方とも行われます。

これらは『実装の多重継承』が引き起こす問題に対する言語毎の対応の結果、という歴史の背景があります。『実装の多重継承』はメリットがある一方で大きなデメリットを生み出す可能性もあるので、Javaでは『型』と『実装』を同時に多重継承することを禁止する、という方法で安全を確保しています。

言語毎で『実装の多重継承問題』に対する対応は異なります。実装の多重継承が許可されている言語(C++PerlRuby)では、Mix-in(ミックスイン)と呼ばれるテクニックがあります。細かくは言語毎に異なる様です。Javaにもその概念がある様ですが、本記事では言及しません。というか、具体的にイメージできなかったので言語化できませんでした。いつかしたいと思います。

変更容易性の実現という目線でみた場合、インターフェイスと抽象クラスの存在意義

オブジェクト指向の三大要素は

ですが、これらの目的は

『変更容易性の高いシステム・ソフトウェア』

の開発です。 開発コストと保守コストがありますが、主に保守コストに対して注目した設計手法なのがオブジェクト指向です。 この中でもっとも重要な考え方が『カプセル化』です。

クラス指向なオブジェクト指向プログラミング言語は、『変更されない箇所を軸に、頻繁に変更されるであろう箇所をクラスに抽出する』ことを想定しています。

変更される場所と変更されない場所を分離することで、「変更されない部分の再利用」そしてより重要な「変更されない部分を利用するプログラムの再利用」が可能になり、変更される側は変更に強い構造にできるようになります。その構造のサンプルとしてデザインパターンが存在しています。

オブジェクト指向は変更されやすい・されづらい箇所を分離しただけではありません。変更されやすい箇所の変更に際し、他の箇所に影響が及ばない様な仕組みを作ってくれています。

上記の双方を実現するための概念が「継承」「ポリモーフィズム」「カプセル化」です。もうひとつ必要な概念があります。

「抽象化」

です。設計段階での抽象化とはちがい、実装レイヤーでの低い場所での抽象化の話になりますが抽象化の作業自体は同じです。抽象化と多態性ポリモーフィズム)には密接な関係があります。

抽象化とは、抽象化したい複数のオブジェクトに、共通で存在する概念や性質を定義・抜き出し、それ以外のものには注目しない、ということです。決して共通の機能を取り出すわけではありません。

テレビのリモコン、エアコンのリモコン、オーディオのリモコン、照明のリモコン

これらの複数のオブジェクトの重要な共通点・性質を考えてみます。これが「抽象化」です。 ざっくりいうなら「生活家電(広義では情報機器)を操作できる」でしょうか。

抽象化は、オブジェクト指向プログラミングで使用される最も基本的な概念の1つです。抽象化は様々な恩恵を与えてくれます。

Java では、抽象化は抽象クラスまたはインターフェースを使用して行われます。2つの主な違いは、抽象クラスは部分的な抽象化も提供できるのに対し、インターフェイスは常に完全な抽象化を提供することです(Java8からは変わった。詳細は後述)。

抽象化を用いる事で「ポリモーフィズム」や「カプセル化」が表現できます。

インターフェイス

インターフェイスに対してプログラミングを行う事でポリモーフィズムが実現できます。この「インターフェイスに対してプログラミングを行う」というのは、抽象メソッドに対してプログラミングを行うという意味になります。

例えば、インターフェイスは『What:なにをするか(できるか)?』と、それを行うのに必要・必須な抽象メソッドが定義されています。ですが、『How:どうやって実現するか?』は定義されていません。その定義を行うためには具象クラスでは、継承した抽象メソッドの具体的な内容を定義する必要があります。

また、クラスやオブジェクトの情報隠蔽を行う際に、使い側と情報隠蔽された側での接続部分にもなります。パッケージやクラスのアクセス修飾子で非公開・公開の設定を制御する情報隠蔽を行う場合、使う側と非公開なプログラムと繋ぐ接点としての公開部分でもあります。

非公開 - 公開(インターフェイス)- 使う側・呼び出し側

の様な形です。

また、内部の詳細を隠して機能を表示する「データの抽象化」も行います。実装の詳細を隠し、機能を外部に提示することが可能です。

まとめると、以下になります。

  1. 呼び出す側と実装の間にインターフェイスが介在することで結合度がさがる(疎結合
  2. 呼び出し側から実装の詳細が隠蔽・分離される(情報隠蔽ポリモーフィズム
  3. インターフェイスや抽象クラスを実装する具象クラス(インスタンス)を入れ替えることで同じ操作で応答を変化させることができる(ポリモーフィズム
  4. カプセル化されたモジュール間での通信経路(情報隠蔽カプセル化
  5. 情報隠蔽が行えることによる開発当初の設計意図の保持(プロダクトの劣化防止)

抽象クラス

抽象クラスでは、複数のオブジェクトで共通する概念を表現するコードを抽出し、それをスーパークラスの具象メソッドとして定義します。簡単にいえば共通部分を括り出して重複を削除しています。大事なのは、その括り出される対象のオブジェクト達の選び方です。

LIP:リスコフの置換原則が定める「サブクラスはスーパクラスと置換可能」であること、括り出される対象のオブジェクト達であるサブクラスと、括り出されたコードの抽出先であるスーパクラスとが「is - a」の関係であることです。つまり、括り出したコード部分はスーパークラス・サブクラスの根幹的な性質である必要がります。これはクラスの継承でも同様のことが言えます。

この「LIP」と「is - a」の文章が成立しないのであれば抽象クラスとしてまとめるべきでもなければ、それを継承してもいけないのです。継承は機能の受け継ぎとして用いられるイメージがつよいかもしれません。その側面もあると思いますが、それのみで継承を行うのは間違いです。また、LIPとis-aの関係は抽象クラスのみならず、継承全般に適用される考え方です。クラスの継承・抽象クラスの継承・インターフェイスの継承全てに当てはまります。

抽象メソッド

抽象メソッドは複数のオブジェクト(サブクラス)がそれぞれで表現したいことを、それぞれのサブクラスで具象メソッドとして定義します。例えば、抽象クラス『人間』を継承するサブクラス「黒色人種」「白色人種」「黄色人種」「黒褐色人種」があったときに、スーパークラスに「肌の色」という抽象メソッドがあります。サブクラスではそれを具象化し、それぞれのサブクラスで「黒色」「白色」「黄色」「黒褐色」と具象メソッドで定義するというとです。色には色の機能があるのでここではメソッドして扱っています。

呼び出し元と呼び出し先のインターフェイス・抽象クラスは同じなのに、インターフェイス・抽象クラスの具象クラスとなるインスタンスによって応答が異なる(具象クラスでの具体的な実装が異なることで)というポリモーフィズムが実現できる様になります。ポリモーフィズムが実現できるのはインターフェイスでも抽象クラスでも同じです。

抽象クラスとインターフェイスには、上記の他に大きな違いがいくつもあります。

  1. 実装の継承か、型の継承か
  2. 多重継承問題に対する立ち位置
  3. メンバ変数の定義が制限されているかいないか
  4. クラスツリーから独立しているか
  5. アクセス修飾子

などなど、上記の様な大きな違いが複数あります。以降からは「継承」「抽象クラス」「インターフェイス」の基本的な部分から説明します。本記事では継承と抽象クラス・インターフェイスをひとまとめにして話してますが、本来は階層の異なる概念なので、ごちゃ混ぜにならない様に注意してください。そうならない様に私も気をつけます........

1.継承とは?

あるクラスの共通点・性質(決して機能目的ではない)を引き継ぎながら新しいクラスを定義することを言います。継承元クラスをスーパークラスもしくは親クラス、継承の結果できた新しいクラスをサブクラスもしくは子クラスと言います。継承には、上記の様に2種類の継承があります。

継承は「機能の継承」を目的とせず、LIP:リスコフの置換原則が定める「サブクラスはスーパクラスと置換可能」なこと、「スーパークラスはサブクラスの一種である」といういわゆる「is-a」の関係を持たなければなりません。 そのため、単に機能を持たせたい場合には、継承ではなくコンポジション(合成)とデリゲーション(委譲)を使用することが原則です。合成と委譲については後述します。

リスコフの置換原則については第2部のインターフェイスについての「2-5-5. インターフェイスの注意点」にて紹介しています。

継承が提供する主な機能は、「実装・型(仕様)の継承」と「型(仕様)の継承」の2つの機能です。特に標準的なJavaの継承(extends)では、この2つの考え方が混在しているため、概念的に分けて考えることが必要になります。

インターフェイスの実装(implements)で継承するのは『型(仕様)』であり 、継承(extends)ではその対象が『型(仕様)』と『実装』になります。

例ですが、StudentがPersonのサブクラスだとした場合、StudentクラスのオブジェクトはStudent型になりますが、さらにPerson型でもあります。学生も人です。その際、実装(継承メソッド・フィールド)と型(仕様)の両方が継承されます。型(仕様)とは、型名・メソッド・戻り値の型などのシグネチャも含みます。

1-1. 継承の目的と注意点

同じものが数多く登場する場合にはグループ化してまとめてしまうべきです。

重複があるとプログラムの修正が広範囲に及ぶことで、修正コストが高くなります。変更カ所が複数に及び、そのうちのたった1つでも修正を忘れてしまうとプログラムは正常に動作しません。重複はプログラムの信頼性を低下させる可能性をたかくします。

さらに、重複のあるプログラムは冗長ですから、人間が読む時にプログラムの「意図」を解釈するコストも増大します。コンピュータはプログラムが読みにくいかどうか、重複があるかどうかなど気にしません。人間の方はプログラム開発する間、数え切れないほどプログラムを読み、解釈し、そのプログラムの挙動を想像します。コード書くより、読む方が多いですよね。なので、プログラムの可読性というのは生産性に直結します。プログラミング中のにコピー・アンド・ペーストを繰り返すのは、保守性の観点から見ると望ましくありません。

継承では、複数のクラスにおいて何度も現れる重複部分を「共通の処理」「不変(変更されずらい)な部分」として抽出し、一つにまとめ(スーパークラス)それをひな型として扱います。その共通の処理の性質を持った新たなクラス(サブクラス)を複数作成させる事ができる。その「共通の処理」部分に変更があった場合はスーパークラス一箇所を変更する事で、サブクラスでもその変更が適用される。これを利用して変更箇所を減らしたり、処理の共通化を行います。 忘れてはいけませんが「is-a」の関係を持たなければなりません。継承とは、そういった関係性をコードで実現するための技術だからです。

また、サブクラスはスーパークラスの「不変」の箇所と比べると「可変(変更されやすい)」処理を表現する場所とも言えます。スーパークラスとの差異である可変な処理をサブクラスで表現していくことを差分プログラミングといいます。

DRYを突き詰め、差分プログラミングを単純な継承(extends)にのみで行おうとすると非常に危険です。 差分プログラミングを実現する方法は、単純な継承(extends)以外にも複数あります。インターフェイスや抽象クラス、コンポジション・デリゲーション・デザインパターンなどです

Efective Java では継承を使用した場合の設計や運用における注意点など、以下などを指定しています。

  • 継承のために設計および文書化する、でなければ継承を禁止する
    • クラスはOverride可能なメソッドの自己利用を文書化(JavaDoc)しなければなりません

クラスが安全にサブクラス化される様にクラスを文書化するには、サブクラス化されなければ示すべきでは無い実装の詳細を記述しなければならない

文書化も何を文書化すべきなのか統一的な見解が必要。文書化するのであれば、外部からみての約束事を書くべきで内部の実装については言及すべきでは無い。Overrideされた数だけ実装は異なる。抽象的な振る舞いや前提条件・約束事・契約事項を書いた方がいい。書くなら。あと、変更修正時のメンテナンス見落としも怖い。

最低でも三つのサブクラスを作成すればテストできる。そのうちの一つか二つは、スーパークラスの作成者以外に書いてもらってください。

全く暗黙知がない状態で実装してもらって、上記の文書で記述した約束事が正しいのか(そもそもスーパークラスに欠陥がないのか)デバッグしてもらう。スーパークラスの作成者は暗黙知があるので、文書化の漏れがあるからです。

これらが表しているのは、継承を用いた設計は相当制限があるし、あるべきだし、結構危ないし、注意点・落とし穴は多いし、正しく運用するのは大変ですよ、ということ。文書化されてる継承なんて見たことない.....。あくまで理想ということだと思いますが、それくらいしないと危険なんだなぁという認識にしています。

継承は便利な仕組みですが、使い方を間違えればメリットを大きく上回るデメリット発生させることになりかねません。以下に特徴と、メリット・デメリットをまとめています。

1-2. 継承(extends)の特徴

  1. 子クラスのインスタンスを親のオブジェクトのインスタンスとして扱うことができる(型の継承)
  2. サブクラスはスーパークラスのフィールド変数とメソッドを受け継ぐ(実装の継承)
    1. private なフィールド・メソッド、及びコンストラクタは引き継げない
  3. サブクラスのコンストラクタ実行時にスーパークラスのコンストラクタの実行が必要(コンストラクタチェイン)
    1. スーパークラスのコンストラクタの明示的な呼び出しには super()を使う
    2. 明示しない場合、コンパイラ時に暗黙的にサブクラスのコンストラクタの最上段で super(); が記述される(コンパイラ時)
  4. スーパークラスのメソッドをサブクラスで override できる
    1. override は「上書き」ではなく『再定義』という意味
  5. クラスの継承(extends)は一度に1つしかできない。Javaでは一度に1つのスーパークラスしか継承(extends)できない(多重継承の禁止)
  6. サブクラスでは、スーパークラスにて final で定義されたフィールドの上書きはできない
  7. 「is - a」が成立する必要がある(AはBである)
    1. サブクラス is a スーパクラス (ex. 人間は動物である)
    2. 車は生き物である、とかはアウト
  8. 継承の正しい運用には以下の前提が必要
    1. スーパークラスに欠陥がない」
    2. 「使用方法が文書(JavaDoc)化されている」 1. (これも欠陥がない前提)
    3. 「使用方法が守られている」
    4. 「階層が深くなりすぎない」
  9. クラスにfinal修飾子を付けると、そのクラスは継承することが出来なくなる
    1. コンパイルするとエラー
    2. Exception in thread "main" java.lang.Error: Unresolved compilation problem: 型 Class~(Sub)~ は final クラス Class~(Super)~ をサブクラス化できません
  10. Java 17 の Sealed Classes では、継承可能なサブクラスを指定できる
    1. 参考:Java 17 の Sealed Classes の書き方
    2. 参考:Java 注目の機能:Sealed クラス

1-3. 継承(extends)のメリット

  1. 複数のオブジェクトで共通している部分を抽出することで、重複を削除できる
  2. コード量を減らすと同時に再利用性を高め、変更箇所を抑えることができる
  3. スーパークラスを元にサブクラスでは機能の拡張ができる(メソッド追加・Override)
    1. 差分プログラミングが実現できる
  4. スーパークラスとサブクラス間に密接な関係が存在する場合は使いやすい

1-4. 継承(extends)のデメリット

  1. サブクラスはスーパークラスの実装に強く依存しているので変更に弱い
    1. 共通箇所(不変)をスーパークラスにまとめるためサブクラスがその実装に依存する
  2. 継承(extends)の階層が深まるのは非常に危険だが、誰でも簡単にできてしまう
  3. リスコフの置換原則・スーパークラスとサブクラスの関係性「is - a」を破ってしまう様な継承(extends)が簡単に行える
  4. 階層が深くなりすぎたり関係性が破られてしまうと、可読性・保守性・拡張性・再利用性は大きく損なわれる
    1. 単一継承といえど複雑化してしまうと、問題発生時の切り分けにも困る
    2. 単一継承はツリー構造で単純な構造です。対して多重継承はネットワーク構造で複雑になりがちです
    3. サブクラスだけでは何をしているか解らないためスーパークラスを常に参照する必要がある
  5. 漫然と使用するといつでもどこでも上記が起こり得る
  6. 継承(extends)はクラスツリー内での実行に制限があるため「組み合わせ爆発」が起こりうる(「抽象クラスとインターフェイスの違い」の項にて説明)
  7. 多重継承問題(後述)
  8. カプセル化の破壊
    1. スーパークラスの実装をサブクラスに公開
    2. スーパークラスの実装を理解し再利用するため、ホワイトボックス再利用と呼ばれます

1-5. 継承よりコンポジション(合成)、もしくは継承よりデリゲーション(委譲)

迷ったときはコンポジション

継承の関係性は「 is - a 」でしたが、それに対してコンポジションは「 has - a 」となります。

「 has - a 」は包含関係を表し、『集約』の一部です。

  • 全体クラス has a 部分クラス という意味です。
    • 「全体クラスは部分クラスを含んでいる」

Effective Javaにも書いてある事ですが、親クラスについての知識が必要で時に親クラスの世界を壊してしまう可能性のある継承よりも、コンポジションで考える方がよりふさわしいという事が示唆されています。

Effective Java には

既存のクラスを拡張する代わりに、既存のクラスのインスタンスを参照する private なフィールドを、新たなクラスに持たせるのです。既存のクラスが新たなクラスの構成要素になるので、この設計はコンポジション(composition)と呼ばれます。

新たなクラスの各インスタンスメソッドは、保持している既存のクラスのインスタンスに対して、対応するメソッドを呼び出してその結果を返します。これは転送(forwarding)と呼ばれ、新たなクラスのメソッドは転送メソッドと呼ばれます。

時折、コンポジションと転送の組み合わせが大雑把に委譲(delegation)と呼ばれることがあります。

なるほど!と思いますが、続く文章で??となります

技術的には、ラッパーオブジェクトがラップしているオブジェクトへ自分自身を渡さない限り委譲ではありません。

と書いてあります。これは本書のコンテキストも理解した上で読み進めないといけないという点もあるのですが、正直意味わからない上にいろんな文献にコンポジションとデリゲーションが同一で扱われていたりするので、うーんという感じです。

転送するときに引数として、this入れとかなあかんでってことかな? これに対して間違いだとおっしゃる記事もありました。

引用元:委譲とコンポジション(Effective Java の間違い)

Effective Java には、自身を参照にして渡さないと委譲とは呼べない、とあるが、これは間違い。Effective Java にも挙げられている [GoF P32] には次のようにある。

委譲では、1 つの要求を 2 つのオブジェクトが扱う。要求を受け取ったオブジェクトは委譲者へオペレーションを委譲する。これは、サブクラスが親クラスに要求を渡すことと同様である。

基本情報技術で委譲についての説明を検索しました。

あるオブジェクトに対する操作を その内部で他のオブジェクトに依頼する仕組み

合成についても

複数のオブジェクトを部分として用いて, 新たな一つのオブジェクトを生成する仕組み

とのこと。この言い方だと以下の様なイメージですね。

Engine engine = new JetEngine();
Transmission transmission = new AutomaticTransmission();
Handle handle = new QuickHandle();
Brake brake = new AntilockBrake();
Wheel wheel = new StudlessWheel();

Car car = new Car(engine, transmission, handle, brake, wheel);

Car クラスのインスタンス生成時に、各部品を表現するインスタンスを引数で渡してますね。Car型のインスタンス生成時のコンストラクタ実行時に private フィールドに対しての初期値を設定します。そのフィールドを用いれば委譲も行えますね。

wikiには

ほしいと思っている機能を、実装した他のクラスのインスタンスを、保持させることによって

という記述があります。分かり易い。

またミノ駆動さんがTwitterでこういってました。

引用元ツイート:クソコード動画「継承」

「継承より委譲」でググると分かるかと思いますが、継承は使わず決済クラスをprivateインスタンス変数として持つ、コンポジションと呼ばれる設計が推奨されます。

1-5-1. 合成と委譲に対する解釈

色々と勘案した結果、現時点での解釈として以下となります。

  • 合成とは:あるクラスにほしいと思っている機能を持つインスタンスを、そのクラス内に 『private』 フィールドとして内包することで、機能の再利用を行う設計
  • 委譲とは:あるクラスで受け取った処理を、合成で包含している他のクラスのインスタンスへ委ね、そのインスタンスのメソッドを呼び出して処理させる。

実際に実装している中でもこの解釈で問題ないと思うのですが皆様のご意見いかがでしょうか?

サンプルコード

// 合成という概念に基づいて設計されたクラス群

// インターフェイス
// インターフェイス型[Message]を規定
// 抽象メソッドとその戻り値・引数・メソッド名(シグネチャ)を規定
public interface Message {
    void write(String text);

    boolean delete(int target):

    boolean send(String text);
}

// インターフェイスの実装クラス
// 抽象メソッドを実装(具象化)する
// implements はインターフェイスから型・実装(具象化)すべきメソッド情報を継承するという意味
public class MessageImple implements Message {
    @Override
    void write(String text){
        ...
    }

    @Override
    boolean delete(int target){
        ...
    }

    @Override
    boolean send(String text){
        ...
    }
}

// エントリーポイントなクラス
public class Mail {
    // 委譲先のMessage(インターフェイス)型の『private』なインスタンス変数
    // 「has - a」の関係(Mailクラスは、MessageImpleインスタンス参照情報を包含)
    private Message message;
    private String title;
    private String body;

    //転送メソッド
    private boolean send(int userId) {
        // 転送 インスタンス変数からメソッド呼び出して結果を返す
        return this.message.send();
    }

    ...
}

また、GoFデザインパターンの Strategy パターンでも合成と委譲が用いられています。 Plyerクラスが Strategyインターフェイスを以下の様に、private なインスタンスフィールドとして包含しています。 Plyerクラスが Strategyインターフェイスを合成(包含)しています。

public class Player {
    // private型のインスタンス変数
    private Strategy strategy;
    private String name;
    private int wincount;
    private int losecount;
    private int gamecount;

Strategy パターンのクラス図
Strategy パターンのクラス図

継承の欠点

  1. サブクラスはスーパークラスの実装に強く依存しているので変更に弱い
  2. 継承の階層が深まるのは非常に危険だが、誰でも簡単にできてしまう
  3. スーパークラスとサブクラスの関係性「is - a」を破ってしまう様な継承(extends)が簡単に行える
  4. 階層が深くなりすぎたり関係性が破られてしまうと、可読性・保守性・拡張性・再利用性は大きく損なわれる
  5. 漫然と使用するといつでもどこでも上記が起こり得る
  6. 継承(extends)はクラスツリー内での実行に制限があるため「組み合わせ爆発」が起こりうる
  7. 多重継承問題
  8. カプセル化の破壊(ホワイトボックスな再利用)

合成・委譲と継承の違い

  • 結合が弱まる(インターフェイスを使用すればより疎結合にできる) -動作を実装する責任を関連付けられたオブジェクトに委任できる
  • 結果、各クラスは各クラスの責務に向き合うことができます
  • クラスが増えるという欠点はある
  • クラス間の関係を表現する柔軟性が高い
  • 影響範囲の特定がしやすくなる

ということで、合成と委譲の解釈として以下が結論です。

合成: あるクラスにほしいと思っている機能を持つインスタンスを、そのクラス内に 『private』 フィールドとして包含することで、機能の再利用を行う設計

委譲: あるクラスで受け取った処理を、合成で包含している他のクラスのインスタンスへ委ね、そのインスタンスのメソッドを呼び出して処理させる

そして、継承は100%悪で、コンポジションが100%正というわけではありません。ただ、継承が“見かけほどには”便利ではないなと感じます。状況に応じてコンポジションと継承、どちらが最適なのかを選べるようになりたいですね。 ただ 「迷った時はコンポジション でいいのかなと思いました。

1-6. 継承(extends)による実装継承はどの程度のレベルで避けるべき?

2003年の古い記事です。今から20年近く前から言われていますね。。。。

引用元:なぜ extends は悪なのか; 具象基底クラスをインターフェイスに置き換えてコードを改善する Why extends is evil; Improve your code by replacing concrete base classes with interfaces


Java の extends は悪だ; チャールズ・マンソンほどでないかもしれないが、 可能ならいつでも避けなければならないほど悪いものだ。 GoFデザインパターンの本は、ページ数を割いて、 実装による継承 (extends) をインターフェイス (implements) による実装に置き換える方法について議論している。

The extends keyword is evil; maybe not at the Charles Manson level, but bad enough that it should be shunned whenever possible. The Gang of Four Design Patterns book discusses at length replacing implementation inheritance (extends) with interface inheritance (implements).

良い設計者は、ほとんどのコードをインターフェイスについて書いていて、 具象ベースのクラスについては書いていない。 この記事は、なぜ設計者がそのような奇妙な習慣を持つのかを説明し、 2、3のインターフェイスベースのプログラミングの基礎について導入する。 Good designers write most of their code in terms of interfaces, not concrete base classes. This article describes why designers have such odd habits, and also introduces a few interface-based programming basics.


かつて私は Java の開発者である James Gosling がゲストスピーカーに呼ばれた Java のユーザミーティングに参加しました。 印象的な Q&A セッションの間、誰かが彼に尋ねました。 「もし、 Java をもう一度開発し直せるなら、何を変えますか?」。彼は答えました「クラスを除きます。」

I once attended a Java user group meeting where James Gosling (Java's inventor) was the featured speaker. During the memorable Q&A session, someone asked him: "If you could do Java over again, what would you change?" "I'd leave out classes," he replied.

笑いが収まった後、彼は本当の問題がクラスそのものではなく、 むしろ(extends の関係である)実装を伴う継承にあることを説明しました。 (implements の関係である)interface による継承が望ましい。 もしできるなら、実装継承は、いつも避けるべきだ。

After the laughter died down, he explained that the real problem wasn't classes per se, but rather implementation inheritance (the extends relationship). Interface inheritance (the implements relationship) is preferable. You should avoid implementation inheritance whenever possible.

1-6-1. 継承は100%悪なのか?

継承は100%悪で、コンポジションが100%正というわけではありません。状況に応じてコンポジションと継承、どちらが最適なのかを選べるようになりたいところです。と、上記ではいいました。

ですが、正直メリットと比較してデメリットの方がやばいと感じます。現在では、継承のメリットをもっと安全な別の方法で実現できるものも多くあります。代表的なのがコンポジションやデリゲーションです。そういった背景から近年では、継承いらないんじゃね?という考えが強くなり、Golang, Rust では継承というシステムがそもそも組み込まれていません。

重複は必ずしも悪なのか?

上記で散々重複は宜しくないと言いましたが、はたしてそうでしょうか?いついかなる時も悪いのでしょうか?

DRY原則を元に、差分プログラミングを思考停止で行うとどうなるかは広く知られていますね。八階層くらいの継承(extends)があって基底クラスの実装変更したら全階層に影響が出る可能性が高いけど、実際にどう影響があるかは実装をみないとわからない....なんて話です。

オブジェクト指向は変更容易性の実現のための手法です。変更しやすいとは、仕様変更時の影響範囲が特定しやすいという特性も含まれています。そのための「継承」なのに本末転倒です。継承による差分プログラミングは確かに開発効率は上がるんでしょうが、そのメリットを大きく上回るデメリットを産み落とす可能性もあります。

散々重複はよくない言いましたが見方を変えてみましょう。重複部分を残した場合は、独立性が担保されるという見方もできます。一箇所を変更しても自身にしか影響が出ません。デメリットは同じコード何回も書かなくてはいけない・コード量が増える、ですね。

何を目的とするかでメリットデメリットは入れ替わります。どんな問題にでも解決策を提供できるような「銀の弾丸」はないってことです。ただ、武器はたくさん持っておくべきでしょう。継承も委譲もその一つです。

継承のデメリットや危険性も散々書きましたが、デザインパターンの中には継承を用いるパターンも良くあります。

  • Abstract Pattern ( extends 使用 )
  • Template Method [ Abstract Pattern 使用 ]
  • Factory Method Pattern [ Template Method Pattern 使用 ]
  • Dependency Injection Pattern [ Factory Method Pattern 使用 ]

などの内部実装には継承が適用されています。

適用すべき・すべきではない、といった区別。インターフェイスのような道具の元々の目的への理解や、現状で解決したい課題に対しのソリューションと、そのトレードオフ。それらを踏まえて上で判断すべきでしょう。

何事も手段を目的化してはいけません。手段にとらわれて目的を見失うのは本末転倒です。何事もケースバイケースのトレードオフです。重複は確かに悪かもしれませんが、思考停止してメリットデメリットを天秤にかけることを怠ると必ずそのツケを払う日が来るんですねぇ.....

1-7. 暗黙的な継承『Object クラス』

Java の全てのクラスの基底クラスは Object クラスです。全てのクラスは Object クラスを暗黙的に継承しています。Object クラスとは全てのクラスの継承元にあたるクラス。したがって、全てのクラスは

  • Object クラスのメンバを呼び出せる
  • Object クラスのメソッドをオーバーライドできる

継承のルール上、サブクラスを他のクラスが継承できる。その場合、サブクラスを継承したサブクラスは、大元のスーパークラスまでの一連のクラスのメンバを継承できます。

また、非常に重要な認識ですがクラスは 「Object クラスを root」とした「ツリー構造」になっています。これによって行いたい継承によっては第四部でお話しする「抽象クラスとインターフェイスの違い」にて後述しますが「組み合わせ爆発」を引き起こす可能性があります インターフェイスはこのツリー構造から独立しているという点で便利なのです。

Object クラスと木(ツリー)構造
Object クラスと木(ツリー)構造

引用元:Chapter 12. The java.lang Package


java.langパッケージには、Java言語にとって最も中心的なクラスが含まれています。図12-1からわかるように、クラスの階層は深いというより広く、つまりクラスは互いに独立しています。


Object は、すべての Java クラスの究極のスーパークラスであり、したがって、すべてのクラス階層の最上位に位置します。Class は、Java クラスを記述するクラスです。Javaに読み込まれる各クラスには、1つのClassオブジェクトがあります。Boolean、Character、Byte、Short、Integer、Long、Float、および Double は、プリミティブな Java データ型のそれぞれを包む不変のクラス・ラッパーです。これらのクラスは、プリミティブ型をオブジェクトとして操作する必要がある場合に便利です。

1-6-1. おまけ Object クラスの代表的なメソッド

  • String toString()
    • オブジェクトが表す文字列を返す
  • boolean equals(Object obj)
    • オブジェクトが引数と同じものであるかどうかを調べる

これらのメソッドは Override される前提で定義されています。

1-7. 多重継承とは?なぜ必要なのか?

現実世界では、人はしばしば会社員であると同時に父親であったり、プログラマであると同時にライターであったりします。一つのオブジェクトが抽象化された性質を複数持つことが普通だということですね。

継承をプログラムの共通部分をくくり出す抽象化の手段として考えてみると、1つのクラスから抽象化(抽出)できる部分が1つだけというのはプログラミングの上で大きな制約になります。このような発想から生まれたのが多重継承で、自然な拡張と考えることができます。単一継承はスーパークラスを1つしか持てませんが、多重継承はスーパークラスを複数持つことができます。

具体的な例を挙げてみます。

大学では、博士課程(PhD)の学生はインストラクターとして働いてもいます。そういう意味では、教員(Faculty)のような存在です(授業のインストラクターを務め、部屋番号や給与番号を持つなどしています)。しかし、彼らは学生でもあります。科目を履修し、学生番号を持つなどしています。これを多重継承を使ってモデリングしてみます。

PhDStudentはFacultyとStudentの両方のサブクラスです。こうすることによって、PhDの学生は学生と教員の両方の属性を持つことができます。概念的には多重継承は非常に単純なものです。

 図1. 多重継承の一例
図1. 多重継承の一例

1-7-1. 多重継承と単一継承の関係性

関係性は「トレードオフ」です。どちらにも良い点と欠点があり、表裏一体になっています。

単一継承の特徴 - 継承関係が単純

継承関係が単純な木構造になります。これは利点でもあり。欠点でもあります。クラスの関係が単純なため混乱を生みませんし、実装も簡単です。ただし、継承関係を越えたコードの共有ができません。それは単一のスーパークラスしか継承できないという制限に基づきます。

多重継承の特徴 - 複数のクラスから機能を取り込むことができる

多重継承は単一継承ができることなら何でも実現できます。しかし、クラス関係が複雑になりがちという欠点があります。単一継承ではツリー構造でしたが、多重継承ではネットワーク構造になり複雑性が上がります。それは単一のスーパークラスしか継承できないという制限がなくなるからです。このため、どのクラスがどのクラスの機能を利用しているのか分かりにくくなりますし、問題が発生したときに、どのクラスとどのクラスが悪さをしているか見分けにくくなります。

1-7-2. 多重継承問題「組み合わせ爆発」

現実世界では、人はしばしば会社員であると同時に父親であったり、プログラマであると同時にライターであったりします。といったこと先に言いました。

もしこれが、会社員であり父親でありプログラマでもありライターでもあり.....でもあり....でもあり....でもあり....、が続いていく場合。これらを全て多重継承で対応すると、「組み合わせ爆発」が発生し、とんでもない複雑性に発展します。ネットワーク構造がもたらす問題性ですね。

詳細については上記と併せ、インターフェイスの項で説明したいと思います。

追記:思ったよりも理解できてなかったので、一旦中止します。

1-7-3. 多重継承問題「ダイヤモンド継承」

ダイヤモンド継承とは以下の図の様な状態です。

図2:ダイヤモンド継承の例
図2:ダイヤモンド継承の例

これは、あるクラス(PhDStudent)が2つのスーパークラス(FacultyとStudent)を持ち、さらにそれらが共通のスーパークラス(Person)を持つというものである。モデリングすると菱形(ダイヤモンド)になります。

最上位のスーパークラス(この場合はPerson)にフィールドがある場合、最下位のクラス(PhDStudent)はこのフィールドのコピーを1つ持つべきでしょうか、それとも2つ持つべきでしょうか?このクラスはこのフィールドを2回継承しています。

答えは、「場合による」です。例えば、問題のフィールドがID番号であれば、博士課程の学生は2つ持つべきでしょう。学生IDと教員/給与計算IDで、異なる番号になるかもしれません。しかし、フィールドがその人の姓である場合、1つしか必要ありません(博士課程の学生は、両方のスーパークラスから継承されているにもかかわらず、1つの姓しか持っていません)。

Java設計者は、継承には単一継承しか認めないが、インターフェイスには(型の)多重継承を認めるという形で解決策を生み出しました。

詳細はインターフェイスの項で説明します。

1-7-4. 多重継承問題のまとめ

上記の問題を分解してまとめると、以下の三点になります。

  1. 構成の複雑化

単一継承では、あるクラスのスーパークラスは簡単に決まります。直接上のスーパークラス、そのスーパークラス、そのまたスーパークラス、…と一列に並ぶ単純な関係です。多重継承では、あるクラスに複数のスーパークラスがあり、その複数のスーパークラスそれぞれにさらに複数のスーパークラスがあるので関係が複雑になってしまいます(ツリー構造→ネットワーク構造に。組み合わせ爆発のリスクも)。

  1. 優先順位

複雑な関係を持つスーパークラスがあるということは、クラス群の優先順位が一目で分からないということです。例えば図のようなクラス階層があるとします。Dがあるメソッドを受け継ぐ順番は、D,B,A,C,Objectなのか、D,B,C,A,Objectなのか、あるいは全く違う順序なのかが分かりません。一つに決まらないのです。クラスの優先順位がはっきり定まる単一継承とは対照的です。

優先順位が定まらない
優先順位が定まらない

  1. 機能・名前の衝突

多重継承では複数のスーパークラスからメソッドなどの機能を受け継ぐことから、受け継いだメソッドの名称が衝突することもあります。同じ名前のメソッドを持つ別のクラスを継承した場合、どちらのメソッドが呼び出されるかわからない。一意に定めることはできません。

多重継承が可能な言語では、このような状況に対応するためのルールや仕組みが必要であり、そのルールは複雑になっています。

1-8. Java における多重継承問題の解決策

型(仕様)継承による解決

多重継承の問題はすべて、メソッドの実装とフィールドといった実装継承に関係しているのが解ります。実装の多重継承は厄介ですが、型の多重継承には問題がないように見えます。実装の多重継承は、委譲(他のオブジェクトへの参照)で代用できるので、それほど重要ではありませんが、型(仕様)の多重継承はしばしば非常に便利で、合理的な方法で簡単に置き換えられるものではありませんでした。

そこで当時の Java 設計者は、実装(コード)には単一継承しか認めず、型(仕様)には複数継承を認めるという解決策を行いました。

それがインターフェイスです。

また、単一継承にはクラス階層問題も発生します。これは、継承がどうしてもクラスツリー構造に依存することが原因です。詳しくは「抽象クラスとインターフェイスの違い」の項で説明しますが、インターフェイスはこの問題を解決することができます。

第一部はここで終了です。

残り3部は骨格は出来ており、肉付をするだけです。ぼちぼち上げていきたいと思います。

次部 「インターフェイス