Понимание принципа инверсии зависимостей
Низкоуровневые модули теперь зависят от абстракций, а не наоборот
Принцип инверсии зависимостей (Dependency Inversion Principle, DIP) говорит, что:
- Высокоуровневые модули (логика) не должны зависеть от низкоуровневых (деталей реализации). Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Плохой код без инверсии зависимостей
Допустим, у нас есть сервис, который работает напрямую с базой данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type MySQLRepository struct {} func (m *MySQLRepository) Save(user string) { fmt.Println("Saving user", user, "to MySQL") } type UserService struct { repo MySQLRepository // Прямая зависимость от MySQL } func (u *UserService) RegisterUser(name string) { u.repo.Save(name) } |
Проблемы:
- Сервис жёстко привязан к MySQL.
- Если нужно заменить MySQL на PostgreSQL, придётся менять код.
- Код сложно тестировать, потому что нельзя подставить фейковую реализацию.
Правильный код с инверсией зависимостей
Сначала создаём абстракцию (интерфейс):
1 2 3 |
type UserRepository interface { Save(user string) } |
Теперь сервис будет зависеть не от конкретной базы данных, а от интерфейса:
1 2 3 4 5 6 7 |
type UserService struct { repo UserRepository // Зависимость от абстракции } func (u *UserService) RegisterUser(name string) { u.repo.Save(name) } |
Теперь реализуем этот интерфейс для MySQL:
1 2 3 4 5 |
type MySQLRepository struct {} func (m *MySQLRepository) Save(user string) { fmt.Println("Saving user", user, "to MySQL") } |
И для PostgreSQL:
1 2 3 4 5 |
type PostgreSQLRepository struct {} func (p *PostgreSQLRepository) Save(user string) { fmt.Println("Saving user", user, "to PostgreSQL") } |
Теперь мы можем легко менять реализацию без изменения кода сервиса:
1 2 3 |
repo := &MySQLRepository{} // Или PostgreSQLRepository service := UserService{repo: repo} service.RegisterUser("Alice") |
Раньше высокоуровневый модуль (UserService) зависел от деталей (MySQL). Теперь детали (MySQL, PostgreSQL) зависят от абстракции (UserRepository).
Это и есть инверсия зависимостей: теперь низкоуровневые модули должны реализовать интерфейс, который диктует высокоуровневый модуль.
Когда мы говорим «API (или БД или любой другой низкоуровневый модуль) зависит от интерфейса», мы имеем в виду, что он реализует этот интерфейс.
То есть, вместо того чтобы логика программы жёстко зависела от конкретного API-клиента или базы данных, мы сначала создаём абстракцию (интерфейс), а потом реализация API или БД «подстраивается» (зависит) под него.
🔹 Пример: как API зависит от интерфейса
Допустим, у нас есть сервис, который получает данные либо из API, либо из БД.
Создаём интерфейс (DataSource
)
Теперь не сервис зависит от API или БД, а API и БД зависят от интерфейса:
1 2 3 4 |
type DataSource interface { GetData() string } |
API-клиент реализует этот интерфейс
Теперь API зависит от интерфейса, потому что оно реализует DataSource
:
1 2 3 4 5 6 7 |
type APIClient struct {} // API-клиент реализует интерфейс DataSource func (a *APIClient) GetData() string { return "данные из API" } |
БД тоже реализует интерфейс
Точно так же БД теперь зависит от интерфейса:
1 2 3 4 5 6 7 |
type Database struct {} // База данных реализует интерфейс DataSource func (db *Database) GetData() string { return "данные из БД" } |
Сервис работает через интерфейс
Теперь наш сервис вообще не знает, API это или БД — он работает только с DataSource
:
1 2 3 4 5 6 7 8 9 |
type DataService struct { source DataSource // Используем абстракцию } func (d *DataService) Fetch() { data := d.source.GetData() fmt.Println("Получены данные:", data) } |
Запускаем код
Теперь можно легко переключаться между API и БД:
1 2 3 4 5 6 7 8 9 |
apiClient := &APIClient{} db := &Database{} service1 := DataService{source: apiClient} service1.Fetch() // Получены данные: данные из API service2 := DataService{source: db} service2.Fetch() // Получены данные: данные из БД |
Вывод
API и БД теперь «зависят» от интерфейса, потому что они обязаны его реализовать.
Сервис теперь не знает деталей реализации, а работает через интерфейс.
Мы можем легко подставлять другие реализации без изменения кода DataService
.
То есть «зависимость» в этом контексте — это не «использование», а «подчинение интерфейсу».