インターフェースはなぜ使うのか?
こんにちは、@nerusanです。
社内でハッカソンを実施することになり、最近はgolnagをよく触っています。
フレームワークはGinを利用しているのですが、Ginはフレームワークの割には、自由度が高いため、アーキテクチャーをしっかり設計をして利用することが大事になってきます。
その中で、golangを含めたオブジェクト指向言語では、インターフェースを利用した設計で開発することが大事になってきます。(golangは厳密なオブジェクト指向言語ではないかもしれませんが。。)
そこで、インターフェースとはなんなのか?なぜ大事なのか?どうメリットがあるのか?を簡単に学んだことを説明したいとおもいます。
(説明中のコードはgolangとTypeScriptを併用しておりますが悪しからず)
インターフェースとは
まず、インターフェースとはなにって言う話です。
インタフェースは、オブジェクト指向プログラミング言語においてサポートされる、実装を持たない抽象型のことです。インターフェースだけでは、処理などを行えず、実体を持たない型っていう感じです。利用するときは、インターフェースの型をもとに、実装してあげる必要があります。これを、実現と言います。
依存関係とは
用語の説明にはなるのですが、プログラミングには、依存関係と言うものが存在します。
あるオブジェクトに対して、他のオブジェクトが存在しないと成り立たない関係性のことです。
以下の例では、ObjectAはObjectBが存在しないと成り立ちません。
ObjectAはObjectBに依存していると言えます。
export class ObjectB {
// objectBの実装
}
import { objectB } from './B.ts';
// ObjectAはObjectBが存在しないと成り立たない
// ObjectAはObjectBに依存している
class ObjectA {
private objectB: ObjekutB;
}
インターフェースに依存
インターフェースに対しても依存が成り立ちます。インターフェースを実装することを実現と言いましたが、これは逆に言うとインターフェースに依存していると言えます。
下記の例で言うとUserRepositoryはIUserRepositoryインターフェースに依存していると言えます。
インターフェースを通して利用
利用側はインタフェースに依存させ、実装もインターフェースをもとに実装(実現)します。
つまり、インターフェースを通して操作することになります。
下記の例では、UserApplicationServiceクラスは、IUserRepositoryインターフェースをとおして、UserMySqlRepositoryクラスを操作します。
ここで、インターフェースフェースを利用するメリットをあげると以下になります。
- 利用者側(アプリーケーションサービス側)はコードを変更せずにリポジトリを変更できるようになる
- リポジトリを実装する人と、アプリケーションサービスを実装する人はお互いの実装を気にせず実装できる(実装の中にはどうだっていい)
詳しく見ていきます。
1点目は、プロジェクトのDBが、MySQLからFirebaseへ変更になったとします。上の図では、UserMySqlRepositoryからUserFirebaseRepositoryに変更します。その時、利用側は、IUserRepositoryにのみ依存しているため、たとえ変更になっても、命名の変更する必要がありません。仮に、UserMySqlRepositoryに依存していると、名前をUserFirebaseRepositoryに変更する必要があります。1点だけだといいですが、規模が大きいと数多くの変更する作業が必要になります。
また、シグネチャも実装者任せにすると、予期せぬバグが出たり、修正の影響範囲が膨大になったりします。
2点目ですが、インターフェースさえ決めておけば、利用側と実現側それぞれ、インターフェースを元に実装するおことができます。つまり、UserApplicationServiceを実装する人と、UserMySqlRepositoryを実装する人、UserFirebaseRepositoryを実装する人、それぞれ異なる人が実装できるということです。平行して作業ができるので、実装の効率も良くなります。
依存関係逆転の原則(DIP)
インターフェースが便利ということがなんとなくわかったと思います。
そこで、インターフェースに関連した原則を紹介します。
SOLID原則の1つで依存関係性逆転の原則があります。
具体的には以下のことを言う原則です。
-
上位レベルのモジュールが下位レベルのモジュールに依存してはならない
- どちらのモジュールも抽象(インターフェース)に依存すべき
-
抽象は詳細(下位レベル)に依存してはならない
- 抽象の主導権は上位レベルに持たせる
上位レベルはよりドメインに近いものです。アプリケーションサービスと、データストアを扱うリポジトリを比べるとアプリケーションサービスの方が上位レベルです。
再度同じような説明になるのですが、以下インターフェースを利用しない場合の例を挙げます。
上記の場合だと、UserMySQLRepositoryをPostgressRepositoryに変更したとき、利用側も全て変更する必要があります。
そこで、インターフェースを利用することで、モジュールを利用する側、利用される側お互い疎結合になり、変更に強くなるかつ、お互いの処理の中身を知らなくても、よくなります。みてわかるようにRepositoryは依存されてる関係(矢印が外より指される方向)でしたが、その矢印の方向が逆になり、依存関係が逆転しています。これを、依存性関係の逆転と言います。
インターフェースを利用する際、適当に利用するのではなく、
インターフェースの主導権は上位レベル持たせ、下位レベルはそれに従うことが大事になります。
例えば、以下の2点でどちらがわかりやすいでしょうか?
1.名前よりユーザを検索する
type UserRepository interface {
FindUser(ctx context.Context, db repository.Query, name *string) (*entity.User, error)
}
2.MySQLというDBでSELECTを利用してユーザ名よりユーザを検索する
type UserRepositoryWithMySQL interface {
SelectUserWithMySQL(ctx context.Context, db repository.Query, name *string) (*entity.User, error)
}
利用者であれば、圧倒的に、1.の方がわかりやすいのはないでしょうか。利用者にとって、MySQLやSELECTなど具体的な保存サービスやSQLなど保存に関する具体的な方法はどうでもいいです。何らかの、サービスやツールなどを利用してユーザ情報を引っ張り出してくれたらいいです。
2.だと具体的なので、わかりにくいです。また、もし仮に、MySQLからPostgressにDBを変更した時を考えます。そうすると、以下の様に修正する必要が出てくるかなって思います。
type UserRepositoryWithPostgress interface {
SelectUserWithPostgress(ctx context.Context, db repository.Query, name *string) (*entity.User, error)
}
そうすると、インターフェースを利用しているのにも関わらず、このインターフェースを利用しているところすべて、変更する必要があります。
仮に、1.をインターフェースに利用していれば、Postgressに変更しても、インターフェースを変更する必要がありません。変更するのは、インターフェースを実装している関数の1箇所のみです。
なので、より上位層にインターフェースを持たせることを念頭に置いて、インターフェースを利用しましょう。
まとめ
インターフェースの大切さがわかったでしょうか。
大規模なアプリケーションになるにつれて、インターフェースが生きてくると思います。
なので、初めの設計時にインターフェースを導入したアーキテクチャーを組むことが比較的に大事になってくるのかなって思います。途中からインターフェースを導入するとかえってわかりにくかったりする可能性が出てきます。
Discussion