LoginSignup
0
1

More than 5 years have passed since last update.

インターフェースの役割について、自分なりの解釈のまとめなおし

Last updated at Posted at 2019-04-21

CleanArchitecture憧れ系なんだけど、凄く重要な要素なのに、Rubyエンジニアだから実感がわかない「インターフェース」について、考え方の整理もかねて、自分なりの解釈を纏めなおしてみた。

インターフェースの役割の復習

依存関係の整理による保守性の確保

「変更の可能性が高いコードをインターフェースの向こう側に閉じ込めてしまう」事で、保守が容易になる。

無インターフェース例

Aクラスは、Bクラスを使っている。
ある時、Bクラスの実装が大幅に変更された。(これをB'クラスとする)
この時、Aクラスのコードは、どうなるか?

もしかすると大幅な変更が必要かもしれないし、もしかすると何も変えずに済むかもしれない。
それを知るためには、AはBにどうメッセージを投げていて、Bはどうそれを受けていて、B'ではどう変更されているのか等、関連するクラスの全てを調べ直す必要がある。

つまりAは、Bの存在自体に、大きな影響を受けていると言える。

この様に、AがBを前提に書かれている場合、「AはBに依存している」という。

コードの再調査は、大変な作業だ。
クラスが2つなら良いが、大規模なシステムになると、依存関係は急激に複雑になる。
Bに依存しているクラスはAだけではなく、5個10個と出て来るかもしれないし(Anとする)、Anの変更も必要であれば、Anに依存しているクラスもまた洗い出し、全て再調査する必要がある。

とてもやっていられない。
なので、この依存関係は、システムが巨大化していく段階のどこかで、あらかじめ断ち切っておく必要がある。

その為に使う武器の一つが「インターフェース」になる。

インターフェース例

AクラスとBクラスを「インターフェース」を用いた実装で考えると、例えば以下の様になる。

  • Aクラスは、Iインターフェースを使用している。
  • Iインターフェースの実態は、Iインターフェースの仕様通りに実装された、Bクラスである。

ある時、Bクラスの実装が大幅に変更された。(これをB'クラスとする)
この時、Aクラスのコードはどうなるか?

何も変えないで済む。
何故なら、BクラスがB'クラスになっても、Iインターフェース自体は何も変更されていないからだ。
B'クラスはIインターフェースの仕様を忠実に守りつづけなければ、Iインターフェースの実態となる事が出来ない。

AはIを使うため、Iに依存している。さらは、BもIの仕様を守るため、Iに依存している。
処理自体ではAがBを使用しているのだが、「依存は、AもBも、Iにのみしている」

BではなくIに依存しているため、AはBの変更の影響を受けない。
影響を受けないから、BがB'になっても、AやAnの再調査は必要が無い。
当然、それらに依存している無数のクラスの再調査も必要が無い。

とても修正が簡単になる。
Bを修正する時は、Bの事だけを考えていれば良い。

まとめ

  • 「変更の可能性が高いコードをインターフェースの向こう側に閉じ込めてしまう」事で、コードが保守しやすくなる。
    • これを実現するために、依存をインターフェースに集める様に実装しておく

依存関係の整理による拡張性の確保

「拡張の可能性が高いコードをインターフェースの向こう側に置いておく」事で、保守が容易になる

無インターフェース例

AクラスがBクラスを使用している。
ある時、新しくC機能が必要になった。
C機能はBクラス(この機能をB機能とする)と良く似た性質を持っており、B機能とは排他的に実行される。

この時、C機能はどの様に実装されるか?

どの様にでも書く事が出来る。

Aクラスに手続き的に追加されるかもしれないし、Cクラスを追加してAクラスから呼び出すのかもしれない。
もしくは、Bクラスの中でB機能とC機能の実行分岐が追加されるのかもしれない。

どういう形でも実現が可能だが、どの場合でも、それぞれのクラスに、大幅な修正や構成変更が必要になる。そして、依存関係は断ち切れていないので、その影響範囲は無限に広い。

これがC機能だけなら良いが、D機能、E機能と拡張機能が追加される様な事があると、修正箇所が爆発的に増え、手に負えなくなる。
機能の拡張は、簡単に行える様にしておく必要がある。

そのための武器の一つとして、ここでも「インターフェース」を用いる事が出来る。

インターフェース例

AクラスとBクラスを「インターフェース」で実装すると考えると、例えば以下の様になる。

  • Aクラスは、Iインターフェースを引数から取得している。
  • Aクラスは、Iインターフェースの関数を実行している。
  • Iインターフェースの実態は、Iインターフェースの仕様通りに実装された、Bクラスである。

ある時、新しくCという機能が必要になった。
C機能はBクラス(この機能をB機能とする)と良く似た性質を持っており、B機能とは排他的に実行される。

この時、C機能はどの様に実装されるか?

どの様にでも書く事が出来る。
が、簡単に解決する方法がある。

  1. Cクラスを、Iインターフェースの仕様通りに実装する。
  2. A機能に引数として渡すIインターフェースの実態を、BもしくはCクラスとする。

この方法であれば、AクラスもBクラスも、何も修正をする必要が無い

Bとはそもそもが別クラスだし、AはあくまでIインターフェースを使用しているのだから、その実態がBでもCでも関係が無い。

どういう形でも実現が可能だが、この実装をした場合に、影響範囲を最低限に抑える事が出来る。
例えD機能、E機能と拡張機能が追加される事があっても、D/Eそれぞれについて、クラスとIインターフェース作成時の分岐を追加するだけで、実装を終える事が出来る。
とても拡張が簡単になる。

機能拡張に必要な作業は、以下の2つとして纏められる

  • 新機能クラスを追加
  • インターフェース作成時に、新機能クラス用の分岐を追加
    • インターフェースの作成場所は、それ専用のクラスとする

まとめ

  • 「拡張の可能性が高いコードをインターフェースの向こう側に置いておく」事で、拡張が容易になる
    • これを実現するためには、クラスをインターフェース経由で呼び出すように実装しておく

※注
(インターフェースではなく、facadeパターンを使う手もあった。この構成であればそれだけで良いのかも)
(インターフェースは引数ではなく、factoryクラスで実態クラスを分岐させる方が綺麗な気もする)

インターフェースまとめ

インターフェースの目的は以下の2つ

  • 「変更の可能性が高いコードをインターフェースの向こう側に閉じ込めてしまう」事で、保守を容易にする。
  • 「拡張の可能性が高いコードをインターフェースの向こう側に置いておく」事で、拡張を容易にする。

そのために必要な実装方針は、以下の2つ

  • 変更の可能性が高いコードは、直接使用するのではなくインターフェース経由で呼び出す。
  • 拡張の可能性が高いコードは、直接使用するのではなくインターフェース経由で呼び出す。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1