Зависьте от абстракций
Принцип инверсии зависимостей гласит: код высокого уровня — ваша бизнес-логика — не должен зависеть от низкоуровневых деталей, таких как конкретная база данных или формат файла. Обе стороны должны зависеть от абстракции: в Go это интерфейс.
«Инверсия» — это направление стрелки. Вместо того чтобы сервис указывал на одно конкретное хранилище, сервис указывает на интерфейс — а каждое хранилище указывает на тот же интерфейс с другой стороны.
Намертво привязано к деталям
Этот сервис пользователей создаёт и использует конкретный DiskStore напрямую. Он работает — но сервис приварен к одному хранилищу. Другое хранилище, более быстрое хранилище, поддельное хранилище для тестов: каждое из них означает правку самого сервиса.
Сервис, приваренный к своему хранилищу
UserService создаёт свой собственный DiskStore внутри себя. Высокоуровневая логика регистрации теперь зависит от низкоуровневой детали — файла на диске.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type DiskStore struct{}
func (d DiskStore) Save(user string) {
fmt.Println("записываем", user, "в файл users.txt на диске")
}
// UserService создаёт своё собственное хранилище — приварен к детали.
type UserService struct {
store DiskStore
}
func (s UserService) Register(user string) {
fmt.Println("регистрируем пользователя:", user)
s.store.Save(user)
}
func main() {
service := UserService{store: DiskStore{}}
service.Register("ana")
}
Инвертируйте зависимость
Решение: сервис запрашивает интерфейс Storage в своём конструкторе и никогда не узнаёт, какое именно хранилище получил. И дисковое хранилище, и хранилище в памяти реализуют этот интерфейс.
Теперь стрелка указывает на абстракцию с обеих сторон — и замена хранилища становится правкой одной строки в том месте, где создаётся сервис, а не внутри него.
Сервис принимает интерфейс
Та же логика регистрации, два разных хранилища. NewUserService(DiskStore{}) или NewUserService(MemoryStore{}) — код самого сервиса при этом не меняется.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
// Абстракция, от которой зависят обе стороны.
type Storage interface {
Save(user string)
}
type DiskStore struct{}
func (d DiskStore) Save(user string) {
fmt.Println("записываем", user, "в файл users.txt на диске")
}
type MemoryStore struct{}
func (m MemoryStore) Save(user string) {
fmt.Println("храним", user, "в памяти")
}
type UserService struct {
store Storage
}
func NewUserService(store Storage) UserService {
return UserService{store: store}
}
func (s UserService) Register(user string) {
fmt.Println("регистрируем пользователя:", user)
s.store.Save(user)
}
func main() {
onDisk := NewUserService(DiskStore{})
onDisk.Register("ana")
inMemory := NewUserService(MemoryStore{})
inMemory.Register("ben")
}
Выгода: тестирование без диска
Поскольку сервис зависит от абстракции, тест может передать ему хранилище в памяти и мгновенно проверить результат — никаких файлов, никакой уборки. Именно инверсия зависимостей делает код удобным для модульного тестирования.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type Storage interface {
Save(user string)
}
// MemoryStore записывает всё сохранённое — идеально для проверок.
type MemoryStore struct {
users []string
}
func (m *MemoryStore) Save(user string) {
m.users = append(m.users, user)
}
type UserService struct {
store Storage
}
func NewUserService(store Storage) UserService {
return UserService{store: store}
}
func (s UserService) Register(user string) {
s.store.Save(user)
}
func main() {
store := &MemoryStore{}
service := NewUserService(store)
service.Register("ana")
service.Register("ben")
if len(store.users) == 2 {
fmt.Println("проверка пройдена: сохранено 2 пользователя")
}
fmt.Println("сохранённые пользователи:", store.users)
}