はじめに
こんにちは!
以前と言ってもだいぶ前ですが「今度こそクロスオリジンとCORSを完全に理解する」という記事を書き、自分でも何度か見返すことがあったのですが、残しておいて良かったと思ったので、依存関係についてもこちらの記事に残しておこうと思います。
依存関係について
ソフトウェア開発では、コードのモジュール化や再利用性を高めるために「依存関係」の管理が非常に重要になってきます。そこでまず、依存関係とは何か?を解説し、依存性の注入(DI: Dependency Injection)と、依存性の逆転(DIP: Dependency Inversion Principle)について解説していきたいと思います。
※サンプルコードはGo言語です。
依存関係とは?
まず依存関係とは、あるモジュールが他のモジュールに依存している状態です。具体的には、クラスや関数が他のクラスや関数を直接利用することを指します。たとえば、以下のコードでは、UserService
が UserRepository
に依存していることになります。(UserServiceのレシーバーであるGetUserName()メソッド内で、UserRepositoryのレシーバーであるFindUser()メソッドを直接呼び出している)
※これ重要なので、しっかり理解してください。
type UserRepository struct{}
func (r *UserRepository) FindUser(id int) string {
return "UserName"
}
type UserService struct {
repository UserRepository
}
func (s *UserService) GetUserName(id int) string {
return s.repository.FindUser(id)
}
問題点:
-
UserService
はUserRepository
の実装に直接依存しているため、UserRepository
を変更または、他のメソッドに置き換えようとするとUserService
も影響を受けます。(ソースを変更しないといけない) - 他のデータソース(例えば、外部API)に切り替える場合、コードの大幅な変更が必要です。
※ここの「変更」というのは他のメソッドに置き換えることも含まれると考える事と理解しやすいかなと。(自分はそうだった)→たとえばMySQL用のメソッドをPostgres用のメソッドに置き換える様なイメージ
依存性の注入(DI: Dependency Injection)とは?
次に、依存性の注入は、オブジェクトが必要とする依存関係を外部から提供する設計パターンです。
これにより、依存関係を動的に変更したりテスト可能性を高めたりすることができるようになります。
先ほどの例をDIを使って改善してみると
DIを使った例
type UserRepository interface {
FindUser(id int) string
}
type DBUserRepository struct{}
func (r *DBUserRepository) FindUser(id int) string {
return "UserNameFromDB"
}
type UserService struct {
repository UserRepository
}
func (s *UserService) GetUserName(id int) string {
return s.repository.FindUser(id)
}
// コンストラクタによるDI
func NewUserService(repository UserRepository) *UserService {
return &UserService{repository: repository}
}
func main() {
repository := &DBUserRepository{}
service := NewUserService(repository)
name := service.GetUserName(1)
fmt.Println(name)
}
改善点
-
依存関係の柔軟性:
-
UserService
はUserRepository
インターフェースに依存するため、具体的な実装(DBUserRepository
)に依存しません。 - 例えば、
MockUserRepository
を作成してテスト時に差し替えが可能です。
-
※先ほどの例で言うとMySQL用のメソッドをPostgres用のメソッドに置き換える場合、NewUserServiceの引数に与えるオブジェクトを変更すれば良いのです。
-
テストが容易:
- 依存関係を外部から注入するため、モックやスタブを使用したテストが簡単にできます。
DIには、以下の方法があります:
- コンストラクタ注入(上記例)
- メソッド注入
- フィールド注入
依存性の逆転(DIP: Dependency Inversion Principle)とは?
依存性の逆転は、高レベルモジュールが低レベルモジュールに依存するのではなく、両者が抽象(インターフェース)に依存するべきであるという設計原則です。これにより、モジュール間の依存関係が柔軟になり、変更に強い設計が可能になります。
依存性の逆転を適用した例
以下は、先ほどのDIの例でDIPを満たしている状態です。
type UserRepository interface {
FindUser(id int) string
}
type DBUserRepository struct{}
func (r *DBUserRepository) FindUser(id int) string {
return "UserNameFromDB"
}
type UserService struct {
repository UserRepository
}
func (s *UserService) GetUserName(id int) string {
return s.repository.FindUser(id)
}
func NewUserService(repository UserRepository) *UserService {
return &UserService{repository: repository}
}
DIPのポイント:
-
UserService
は具体的なDBUserRepository
に依存していません。代わりに、UserRepository
という抽象に依存しています。 - これにより、低レベルモジュール(データベース接続やAPIコールなど)が変更されても、高レベルモジュール(ビジネスロジック)は影響を受けません。
DIとDIPの関係
- DIはDIPを実現する手段の一つです。依存性の注入を使うことで、モジュール間の依存関係を抽象に向けられ、依存性の逆転を達成できます。
- DIPを適用すると、DIが自然と組み込まれるケースが多いため、両者は密接に関連しています。
まとめ
- 依存関係: モジュール間のつながりを指し、適切に管理しないと柔軟性が失われます。
- DI(依存性の注入): 依存関係を外部から注入することで、テスト可能性や柔軟性を向上させる設計手法。
- DIP(依存性の逆転): 高レベルモジュールと低レベルモジュールが抽象に依存するように設計する原則。
これらの概念を理解し活用することで、変更に強く、再利用性の高いソフトウェアを構築できます。ぜひ、日々の設計に取り入れてみてください!