よしたろうブログ

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

『旧版:Javaの継承と抽象化~ 第2部 インターフェイス ~』

はじめに

※こちらは旧版になります。改訂版は以下のリンクを参照ください

『決定版:Javaの継承と抽象化~ 第2部 インターフェイス ~』

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

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

  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. 情報隠蔽が行えることによる開発当初の設計意図の保持(プロダクトの劣化防止)

抽象クラス

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

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

この「is - a」の文章が成立しないのであれば抽象クラスとしてまとめるべきでもなければ、それを継承してもいけないのです。継承は機能の受け継ぎとして用いられるイメージがつよいかもしれません。その側面もあると思いますが、それのみで継承を行うのは間違いです。

抽象メソッド

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

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

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

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

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

2. インターフェイスとは?

Javaインターフェイスが使用される理由

継承(extends)による多重継承問題をインターフェイスがどう解決するのかを説明する前に、インターフェイスの基本を説明します。

また、基本的に Java8 defaultメソッド機能が追加される前のインターフェイスについて話しています。 Java8 以降のインターフェイスについても後述します。

インターフェイスは上記の通り多重継承問題の解決のために登場しましたが、カプセル化ポリモーフィズムを表現するためにも用いられます。上記も交えてインターフェイスの説明をしていきたいと思います。

非常に簡単に言えば、何かと何かの『境目』の部分・別のものと別のものを繋ぐための『接点』のことを表し、同時に『規格』『ルール』を表現します。現実世界でもプログラミングの世界でも同じ概念です。インターフェイスは外部からクラスを呼び出す時の接点となります。外部から呼ばれることを前提としているので、アクセス修飾子は public のみとなっています。

プログラミングの世界では何と何を繋ぐのでしょうか?クラスとかオブジェクトですね。

2-1. インターフェイスの特徴

  1. メソッドは抽象メソッドしか(元々は)持てない → 実装がない
    1. 暗黙的に public abstract が付与される
    2. 抽象メソッドは、デフォルトで public および abstract
    3. Java8 では具象メソッドの保持も可能に。ただし抽象クラスとは目的が異なる(後述)
    4. Java9 では private な具象メソッドも持てるようになった
  2. インターフェイスを implements した具象クラスでのみ実装ができる、抽象メソッドのオーバーライドを強制
    1. override しないとコンパイルエラー
    2. override は「上書き」ではなく『再定義』という意味
    3. Java8 では具象メソッドの保持も可能に。ただし抽象クラスとは目的が異なる(後述)
    4. Java9 では private な具象メソッドも持てるようになった
  3. 直接インスタンス化できない
    1. 匿名クラスを使用すると近いことはできる
    2. 関数型インターフェイスラムダ式でも近いことはできる
  4. 多重継承ができる
    1. 実装クラスは複数のインターフェースを implements し具象化することができる
  5. コンストラクタの記述はできない
    1. 初期化すべきフィールドは持てない
  6. 定義できるフィールドは static final な定数のみで宣言と同時に初期化が必須
    1. 暗黙的に public static final が付与される
    2. 静的(static)とはインスタンス化しなくてもアクセスできるという意味
    3. 最終(final)とは上書きできないという意味。
      1. 余談だけど、参照型は参照先(メモリ)が final になるだけ
  7. 設定できるメンバ
    1. 抽象メソッド
    2. 最終静的定数フィールド
    3. default メソッド(Java8 以降)
      1. 既存のインターフェースに関連して何らかの機能をまとめておきたい時
    4. static メソッド(Java8 以降)
      1. 実装クラス側で明示的に実装されない場合既定で採用される実装
    5. private な具象メソッド (Java9 以降)
  8. インターフェイス自体のアクセス修飾子は public のみ
    1. どこでも使える様でなければ意味なし

2-2. インターフェイスのメリット

  1. 多重継承(型のみ)が可能
  2. 具象依存から抽象依存を実現
  3. 処理の中の可変を抽出しモジュールを安定化する
  4. データ抽象化により実装が隠蔽される(実装の複雑さを減らすため)
    1. カプセル化情報隠蔽による実装の隠蔽はデータ保護のため
  5. クラス間・パッケージ間での疎結合を実現できる
  6. ポリモーフィズムが実現できる
  7. 複数人の大規模の開発において、共通処理をするメソッド名に統一性を持たせ、処理の画一性をある程度制御できる
  8. メソッド実装漏れ、メソッド名の間違いがあればコンパイルエラーされるのでコーディングミスを防ぐ

2-3. インターフェイスのデメリット

  1. あとからの変更が困難
  2. 特に公開APIとして世界に公開されている場合は一つの変更が世界中に影響する
  3. インターフェースが無闇に生産されると複雑化し管理コストが大きくなる
  4. インターフェイスによる擬似的な多重継承では実装の継承はできない

引用元:Javaの知られざる欠陥(下)

ただしインタフェースによる疑似多重継承には,致命的な欠陥がある。インタフェースでは実装を継承できない。Rubyの作成者であるまつもとゆきひろ氏は「多重継承の代わりにインタフェースを使ったのはJavaの賢いところ。ただし,実装の継承を落としたことはとても痛い」と指摘する。

二つのクラスの機能を引き継ぐ場合,Javaではどちらかのクラスのソース・コードをコピーすることになる。この結果,同じコードがさまざまな個所に分散し,保守性が極端に低下する。

同じ処理をクラスという単位にまとめて保守性を上げようとするオブジェクト指向の利点が生かせない。「マーケティング的には,Javaは大成功を収めた言語だ。しかしプログラミング言語の実装という意味では失敗作だと思う」(まつもと氏)。

2-4. インターフェイスの目的と注意点

目的は、主に抽象化と可変の抽出による安定化だと理解しています。

もっと大きな括りで言えば『変更容易性』の実現です。

  1. 呼び出す側と実装の間にインターフェイスが介在することで結合度がさがる(疎結合
    1. 呼び出し側と呼び出される側の分離
  2. 呼び出し側から実装の詳細が隠蔽・分離される(データ抽象化・ポリモーフィズム
    1. 呼び出し側から見えるのは役割(仕様)だけになり実装が見えなくなる
  3. インターフェイスや抽象クラスを実装する具象クラス(インスタンス)を入れ替えることで同じ操作で応答を変化させることができる(ポリモーフィズム
  4. カプセル化されたモジュール間での通信経路(情報隠蔽カプセル化
  5. 具象依存から抽象依存へと関係性を逆転できる
  6. 情報隠蔽が行えることによる開発当初の設計意図の保持(プロダクトの劣化防止)
    1. 設計思想を理解していなくてもインターフェイスによる制約によってある程度は制御できる

カプセル化とは

  • 設計段階においては、扱うデータと処理をひとまとめに表現(最も重要)
    • これにより、仕様変更時の影響範囲を限定させることができる
    • ビジネスロジックでアクセサメソッドを使用するとカプセル化を壊す危険性がある
  • カプセルの中で扱っているデータを隠蔽し外部からアクセスできない様にする 情報隠蔽といいカプセル化を維持する
    • アクセス修飾子を用いて外部からの可視性を制御 → フィールドを private にすることが該当
  • setter / getter を用いて値を取り扱う
    • 結構危険です。セッターのせいで可変を扱うことになるし、getterが参照型を扱う場合も可変を扱うことになります。つまりカプセル化が不完全になります。
      • 参照型はあくまでメモリを見ている。値の変化には関係がない。
    • 可変な値・オブジェクト扱いはバグを発生させやすいです
    • 原則、使わないが望ましい

外部公開のインターフェイスAPIとも呼びます。インターフェイスさえ見ればクラス内部の実装を見なくてもクラスを扱うことができます。

ポリモーフィズムとは

あるオブジェクトを使用する側がそのオブジェクトの扱い方を変えていないのに実際の応答は異なることです。これのメリットは、使用する側のプログラムと使用される側のプログラムとが分離され、どちらかの変更があったときに片方は影響を受けなくなることです。インターフェイスや抽象クラスの具象クラスとなるインスタンスによって異なる挙動を実現できることです。噛み砕くと、「あるオブジェクトへの操作が呼び出し側ではなく呼び出された側のインスタンスによって定まる特性」です。難しいですね。保守性・可読性・拡張性を高めることができます。

先程、インターフェイスは『接点』だといいましたが、それと同時に『規格』『ルール』も表現します。インターフェイスは、具体的な処理内容は具象(実装)クラスとなるインスタンスに委ねていて、どんな振る舞いをするべきなのかだけが決められたクラスの雛形です。

例えば、

「コンセントって差し口あって、海外では3個だったりするけど日本では差し口2個だよね。ほんで、充電ケーブル刺したら電気が返ってくるよね。」

という様なルール・イメージ・お約束事を表現するのもインターフェイスの役割です。「規格」とう表現は非常に的確だと思います。この規格の元で製造されるのがコンセントです。日本では差し口二個のコンセントが、海外では差し口三個のコンセントが出来上がるわけですが、形状がなんであれコンセントに期待している振る舞いは『コード付きのプラグをコンセントに差し込む』という事前契約を果たしたら『電気を返す』という事後契約が『守られる』ことですね。

オブジェクト指向では規格と、その規格の元に作られる製品を分けています。規格を表現するのがインターフェイスです。

インターフェイスに対してプログラミングを行う事でポリモーフィズムカプセル化が実現できます。この「インターフェイスに対してプログラミングを行う」というのは、抽象メソッドに対してプログラミングを行うという意味になります。つまり、抽象メソッドを具象化することで実装の詳細をコーデイングするということです。このことを『抽象依存の原則』と呼びます(抽象クラスも同様です)。

『抽象依存の原則』は抽象化された概念を表すオブジェクトに依存するという意味です。

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

呼び出し側はインターフェイスを使用するだけで期待する振る舞いが返ってきますが、抽象メソッドを具象化するインスタンスが異なればその振る舞いを実現する実装の詳細が異なります。

上記の例で言えば、抽象メソッドの実装とは、差し口の数が異なるコンセントを作るという部分です。 コンセントに期待する役割は変わりません。電気をくれるならなんでもいいわい、ということですね。

実装(コンセントの差し口の数)が変わっていても、規格 = インターフェイス(コード付きのプラグをコンセントに差したら電気を返ってくる)は変わっていないので(ポリモーフィズム)、使う側からすると実装が変わってようが意識する必要がありません(カプセル化)。

機能の差し替えは呼び出された側のインスタンスの差し替えで実現できます。異なる挙動を後から追加することも実現しやすくなります。これもポリモーフィズムです。

※ 上記で、契約うんぬん書きましたが「契約プログラミング」という概念からきてます。過去記事に書きましたので興味あればご覧ください

2-4-1. インターフェイスで行えること

下記のイラストで具体的な内容を載せています。

①『new がポリモーフィズムを破壊する図』

②『インターフェイスポリモーフィズム実現』

③『インターフェイスを挟む図』

大雑把に言ってしまえばインターフェイスとは実装に注目せず、役割を表現するものです。呼び出す側が知りたいのは実装の詳細(コード)そのものではなく、その実装がどんな機能を実現できるのか?なのです。抽象化を用いない一般的な Class での実装では、呼び出し側と呼び出された具象クラスの関係性が非常に強い結合関係(密結合)を持ちます。呼び出した側は、そのクラスの役割と実装を知ってしまう形になります。インターフェイスは具象クラスの機能を抽象化・抽出し(具体的には仕様・型)したもので、実装はインターフェイスの具象クラスに抽出します。

プレゼンテーション層(以降P層)に属する ControllerClass がビジネスロジック層(以降BL層)に属する ServiceClass を new によって呼び出すのではなく、ServiceClassを具象クラスと(implements)する様な インターフェイスを間に置いて、P層ではそのインターフェイスを介してインスタンスを扱う様にする。これはBL層とデータアクセス層(以降DB層)でも同じ。

ただ、注意点として図ではインターフェイスはレイヤー(層)間にあるが、実際はそれぞれのレイヤーに包含される。図では分かりやすさとしてそう表現してますがイメージのしやすさを優先しているので不正確です。

これによって、P層の ControllerClass ではBL層の ServiceClass のインスタンスをあたかもインターフェイスとしてポリモーフィズムを使って扱える様になる。この様にインターフェイスを介することで呼び出す側(P層のControllerClass)は具体的にどのクラスのインスタンスが動作するかが解らなくなります。知る必要がなくなるのです。これが抽象化であり、実装との依存分離であり、データ抽象化(呼び出す側からは実装が見えないが役割だけはわかっている)、でありポリモーフィズムと呼ばれるものです。

2-4-2. インターフェイスで可変を抽出する

オブジェクト指向

『不変を軸に、可変であろう箇所をクラスに抽出する』

ことを想定してます。

差分プログラミングは、共通部分を抽出しそれを継承することでした。オブジェクト指向は不変だけではなく可変部分の抽出も行うことができます。

以下は、先日9/3に行なったLTで作成した画像です。

不変な部分の抽出はわかりやすいです。そのまま継承の話になります。以下では重複してる箇所を不変・変更されずらいという前提で話しています。

わかり辛いのが可変の抽出部分ですね。

可変な処理が流れの何処かで存在するとこのモジュール全体で、安定性が低下します。可変な部分を不変として扱うことができればこのモジュールは安定しますね。

そんな時に使えるのがインターフェイスです(抽象クラスもなんです)。 可変な処理というのは場合によって行いたいことが変わる部分ですが、処理の中の一部分であるという俯瞰した目線でみると、期待する役割自体は同じであることが多いと思います。例えば処理の順序は同じだけど、場合によっては実装の詳細が異なるなどです。そんなときはインターフェイスなどで役割を抽象化して仕様と実装を分離してあげればいいのです。 場合によって実装の詳細が異なるものはインターフェイス(もしくは抽象クラス)の具象クラスとして個別に独立して定義してあげます。 個別に定義された具象クラスを抽象化したものがインターフェイスです。実際にはここではインターフェイスと抽象クラスによる抽象化でなくても extends による継承でも実現できますが、基本的には実装の継承は使わない方がいいと考えます。詳細には前回の第一部にて記述しております。 可変な処理を外に抽出し、場合ごとの実装も個別に定義したことによって具象クラスのインスタンスインターフェイスとして扱うポリモーフィズムが可能になりました。インターフェイスを呼び出せば可変だった実装が呼び出せる様になりましたので処理の流れも以下の様に変更されます。

これにより、処理の流れの中から可変が消えて全体として安定することができました。インターフェイスは振る舞いを定義しているだけです。役割・型・仕様・規格・ルールなどと表現できますが、抽象的なことしか定義してませんので実装を持たなくていいです。つまり安定しています。

更に機能追加を行いたい場合は、既存の具象クラスを修正変更するのではなく、新しい具象クラスを定義します。これにより処理の流れを可変と不変が入り混じっている状態から不変のみの状態に変更することができました、

また最後の画像では、仕様変更に対する対応は新しい具象クラスを追加することで対応しています。既存の具象クラスも追加された具象クラスもインターフェイス型のインスタンスとして扱うことができます。これにより仕様変更に強い設計が可能になります。このことをオープン・クロースド原則( Open/closed principle)と言います。オープンにするのは拡張(機能追加)に対して、クローズドにするのは修正(機能追加の為に既存のコード(責任)を修正する)に対してです。

また、インターフェイスの具象クラスは型(仕様)の継承(implements)を行なっています。処理の流れとしてもインターフェイスに矢印の向きが集中する形に変更されました。これを依存性逆転の原則( Dependency inversion principle)、もしくは抽象依存原則とよびます。具象(実装や詳細ともいう)に依存してはいけないということです。

 

2-4-3. インターフェイスの注意点

インターフェースの変更は困難

一度リリースされたpublicなインターフェースに対し、後から変更を加えることは非常に難しい。 このような問題を回避する為、正式リリース前にできるだけ多くのプログラマにこのインターフェースを使って実装してもらい、欠陥を早期に発見できるようにすると良い。インターフェースは最初から正しい設計をする必要がある。

初期にインターフェースの設計をミスると、いざ改修が必要になった時には既に手遅れになっているような場合も多く、その場合は大いなる苦しみを伴いながら作りかえを行うことになります。 インターフェースの設計がその後のシステムの保守性・堅牢性の明暗を分けると言っても過言では無いため、インターフェースを設計するときは、抽象度は本当に適切なのか?余計な情報まで受け取ったり渡したりしてしまってないか?など、慎重に考えるべき。このことは Effective Java でも語られています。

インターフェース分離の原則 'Interface Segregation Principle (ISP)'

抽象クラス、基底クラス、全て含めたインターフェースとしての働きを持つもの全てを指します。今まで extends としての具象クラス・抽象クラス、implements としてのインターフェイスで分けて語りましたがこの原則では全てをインターフェイスとして取り扱います。

太ったインターフェースを使うのではなく、粒度の細かなインターフェースを使うべきという原則です。ある1つのインターフェースに何もかもぶち込むのはアンチパターンなので、1つのインターフェースには最小限のものだけを定義しておき、別の役割をもったインターフェースを作るべきです。

クライアントとなるクラスやインターフェースが複数あって、それぞれ必要なメソッドが違うのに、1つのインターフェースで実装してしまうと、関係のないメソッドの影響を受けかねないというものです。

この辺りは Fat Controller と同じ問題ですね。

依存性逆転の原則 'Dependency Inversion Principle (DIP)'

上位モジュールが下位モジュールに依存してはならない、抽象が実装に依存するのではなく実装が抽象に依存すべきという原則です。

ダメな例:

  • 上位モジュールが、必要なメソッドを下位モジュールが提供しています。
  • 下位モジュールで、たとえばシグニチャの修正が発生したとき、上位モジュールを修正しなくてはなりません。

依存性を逆転させた例:

  • 上位モジュールが必要なメソッドのみを含む、抽象であるインターフェースを用意します。
  • 下位モジュールはそのインターフェースを実装します。
  • こうすると、上位モジュールがシグネチャの所有権を持つことになり、依存性が逆転します。

つまり、インターフェイスで型(仕様)を継承し具象クラスで実装を定義すれば勝手にこの原則を実現できます。実装への依存がなくなり、抽象への依存が可能になります。

抽象に依存する様な設計が望ましいのです。その理由はもうすでにいくつも書いてきました。

2-5. クラスとインターフェイスの違い

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

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

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

「型」+「実装」= クラス

となります。それに対して仕様情報だけを切り出したのがインターフェイスです。 多重継承による問題は実装の継承に由来するものでした。

インターフェイスとは 『型(仕様)情報のみを提供するもの』 です。

なので実装するためのクラスが別に必要になります。それがいわゆる実装クラスです。インターフェイスは抽象化が行われたものなので、実装クラスのことを具象クラスとも表現できます。

本記事ではインターフェイスの実装を、型(仕様)の継承(implements)と読んでいます。

※ Java8 での default メソッド機能のサポートによって具象メソッドの実装が可能になったことに伴い、実装の継承も行えるようになりました。これについては後述します。

  

2-6. Java8 で default メソッドが追加された経緯

そもそもなぜ具象メソッドを定義できるようにしたのでしょうか? 以下の Oracle の公式ドキュメントに記述がありますが、わかりずらいです。

https://docs.oracle.com/javase/tutorial/java/IandI/nogrow.html

Java Magazineの2018年1月/2月号に掲載された内容が Oracle のHPで確認できたのでそちらを以下に引用しています。翻訳時に理解しずらい言い回しなど編集してます。原文原文読みたい方は引用元へどうぞ!

参考・引用元:The Evolving Nature of Java Interfaces

Java 8による新しい夜明け
これまで、Java 8で導入された新機能をあえて無視してきました。これは、Java 8が、この言語の以前の設計上の決定(「コードは単一継承のみ」など)と矛盾する機能を追加したため、いくつかの構成要素の関係を説明するのが非常に難しくなったからです。例えば、インターフェースと抽象クラスの違いと存在の正当性を論じるのは、かなり厄介なことになります。後ほど紹介しますが、Java 8のインターフェースは拡張され、より抽象クラスに近いものになりましたが、微妙な違いがあります。

この問題の説明では、まずJava 8以前の状況を説明し、次に新しいJava 8の機能を加えるというように、歴史的な経路をたどってきました。これは、現在の機能の組み合わせの正当性を理解するためには、このような歴史を踏まえて初めて可能になるからです。

もし今、JavaチームがゼロからJavaを設計し、後方互換性を壊すことが問題でないとしたら、同じように設計することはないでしょう。しかし、Java言語は理論的なものばかりではなく、実用のためのシステムなのです。そして、現実の世界では、これまでのものをすべて壊さずに、言語を進化させ、拡張する方法を見つけなければならないのです。Java 8では、デフォルトメソッドとインタフェース内の静的メソッドの2つが進化を可能にする仕組みとなっています。

インタフェースの進化
Java 8の開発で問題になったのは、インタフェースをどう進化させるかでした。Java 8では、Java言語にラムダやその他いくつかの機能が追加され、Javaライブラリにある既存のインターフェースのいくつかを適応させることが望ましいとされました。しかし、このインタフェースを使用している既存のコードをすべて壊さずにインタフェースを進化させるにはどうしたらよいでしょうか。

既存のライブラリにインターフェースMagicWandがあると想像してください。

public interface MagicWan
{
    void doMagic();
}

このインターフェイスは、すでに多くのプロジェクトで多くのクラスによって使用され、実装されています。しかし、あなたは今、本当に素晴らしい新機能を思いついたので、本当に便利な新しいメソッドを追加したいと思います。

public interface MagicWand
{
    void doMagic();
    void doAdvancedMagic();
}

そうすると、この新しいメソッドの実装を提供する必要があるため、以前このインターフェイスを実装していたすべてのクラスが壊れます。つまり、一見すると、既存のユーザーコードを壊すか(これはやりたくない)、簡単に改良する機会もなく古いライブラリに固執する運命にあるかのどちらかで、行き詰っているように見えます。(実際には、サブインターフェイスインターフェイスを拡張するなどの他のアプローチもありますが、これには独自の問題があり、ここでは触れません)。Java 8では、既存のコードを壊すことなく既存のインターフェイスに追加できるという、 両者の長所を得るための巧妙なトリックを思いつきました。これは、デフォルトメソッドと静的メソッドを使って実現されており、これから説明します。

デフォルトメソッド
デフォルトメソッドは、デフォルトの実装であるメソッド本体を持つインターフェイスのメソッドです。メソッドシグネチャの最初に default 修飾子を使って定義し、完全なメソッドボディを持ちます。

public interface MagicWand
{
    void doMagic();
    default void doAdvancedMagic()
    {
        ... // some code here
    }
}

このインターフェイスを実装するクラスは、このメソッドに対して独自の実装を行うか(オーバーライドする)、このメソッドを完全に無視するか(この場合、インターフェイスからのデフォルトの実装を受け取る)、どちらかを選択できるようになりました。古いコードは引き続き動作し、新しいコードはこの新しい機能を使うことができます。

要するに、新しい機能を追加したくなったけど現状のインターフェイスの仕様ではとんでもない問題が発生するからその問題を抑えつつ、新機能を追加するためにはどうすればいいのか?という問いの答えが「defaultメソッド」だったということですね。

新機能はどうやら Stream API のことのようです、多分。

引用元:【詳解】抽象クラスとインタフェースを使いこなそう!!

java8からはStream APIが使えるようになりました。 ※Stream APIの使い方についてはこちらを参照してください。 Stream APIはコレクションの操作(Listの操作とか)ができるようになってます。 この操作をするためにListインタフェースに型を定義(Stream APIなど外から使えるように)して、具体的な処理は実装クラスに書くことになると、継承しているクラスを全て書き換えたりとても大変なことになります。 この互換性を失わないためのものとして出来たのがdefaultメソッドです。

Listインタフェースを見てみると実際にdefaultメソッドが存在しました。

default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }

Java の default メソッド あれだけ実装を伴う継承はダメって言ってたのに、いいのでしょうか?

interface が実装を持てるようになったということは、 実装の多重継承までできてしまうことを意味しています。

やはり書籍 Effective Java によると、使うにしても慎重になと言うことらしいです。

default メソッドは既存のインターフェイスにメソッドを追加できますが、 それをするのはとても大きなリスクが存在します。

While default methods make it possible to add methods to exsting interfaces, there is great risk to doing so. Effective Java - Item 21: Design interface for posterity

2-6-1. Java8 default メソッドと多重継承問題の整合性

多重継承問題は実装の継承に依存しています。それに対する解決策がインターフェイスでした。仕様の継承のみを提供します。ところがdefault・staticメソッドによって具象メソッドを内包することができる様になり、実装の継承が可能になりました。

また、これにより抽象クラスとインターフェイスの違いが非常に曖昧になりましたが依然として中核的な違いが残っていて、抽象クラスとは全く目的が異なります。default メソッドは Java7 と Java8 の互換性のため生まれました。抽象クラスのように共通の性質をまとめるためのものではないので、抽象クラスとは全く別物です。これについては『抽象クラスとインターフェイスの使い分け』の項で説明します。

多重継承問題の解決のために登場したインターフェイスですが実装の継承が可能になってしまいました。default メソッドが追加されたインターフェイスは多重継承問題に対してどの様に解決策を提供したのでしょうか?

Java Magazineの2018年1月/2月号に掲載された内容が Oracle のHPで確認できたのでそちらを以下に引用しています。翻訳時に理解しずらい言い回しなど編集してます。原文原文読みたい方は引用元へどうぞ!

参考・引用元:The Evolving Nature of Java Interfaces

Javaの設計者が、コードの多重継承を避けるために非常に慎重に行動していたことを指摘しました。では、今はどうなっているのでしょうか。この問題に対して、Java の設計者は例によって以下のような実用的なルールを考案しています。

  1. 同じ名前の抽象メソッドを複数継承しても、それらの抽象メソッドは同じメソッドと見なされるため問題ない
  2. フィールドのダイアモンド継承は、インターフェースに static final な定数以外のフィールドを含めることができないので回避できる
  3. 静的メソッドや static final な定数の継承は、使用時にインターフェイス名を接頭辞にしなければならないので名前は衝突しない
  4. 同じシグネチャで異なる実装の複数のデフォルトメソッドを、異なるインターフェイスから継承することは問題だがコンパイラエラーが発生する

結論 インターフェイスJavaの強力な機能です。プログラムの異なる部分の間の契約の定義、動的ディスパッチのための型の定義、型の定義とその実装の分離、Javaでの多重継承の可能性など、多くの場面で役に立ちます。これらはコードの中で非常によく使われるので、その挙動をよく理解しておく必要があります。 Java 8の新しいインターフェース機能、default メソッドなどはライブラリを書くときに最も役に立ちますが、アプリケーション・コードで使われることはあまりありません。

インターフェイスを内部で使用するだけなら影響は少ないのでしょうが、外部に公開し世界中の人間が使用しているようなAPIとしてのインターフェイスの変更は困難だという良い事例ですね。インターフェイスの変更は、その変更が破壊的な変更をもたらすのか?外部に公開しているインターフェイスなのかどうか?によって全く意味が異なりますね。

外部向けAPIとしてのインターフェイスは一旦リリースしたら変更とかほぼ不可能だから、最初に正しく設計することと徹底的なテストが重要だと。Effective Java では説明されています。

ということでインターフェイスの話はここで一旦終了になります。 次は抽象クラスについてはなし、最後にまとめとして抽象クラスとインターフェイスの使い分けを説明したいと思います。