Внедрение зависимостей
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 45 из 107.
Внедрение зависимостей (Dependency Injection) — это метод, при котором struct получает свои зависимости извне, а не создает их внутри себя. В Go интерфейсы делают этот паттерн естественным и мощным.
Вместо жесткого кодирования конкретной реализации внутри структуры, вы принимаете интерфейс. Это позволяет менять реализации без изменения кода структуры:
type Notifier interface {
Send(message string) string
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
Теперь создайте структуру, которая зависит от интерфейса, а не от конкретного типа:
type OrderService struct {
notifier Notifier // зависимость внедрена через интерфейс
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
OrderService не знает и не заботится о том, использует ли он email или SMS. Вы внедряете зависимость при создании сервиса:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Заказ размещен: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Заказ размещен: Phone
}
Этот подход делает ваш код более гибким и тестируемым. Во время тестирования вы можете внедрить mock-уведомитель, который на самом деле не отправляет сообщения. В рабочей среде вы внедряете реальную реализацию. Структура (struct) остается неизменной в обоих сценариях.
Задание
ЛегкоДавайте создадим систему логирования, которая демонстрирует мощь внедрения зависимостей (dependency injection). Вы создадите сервис, который может записывать логи в разные места назначения, при этом сам сервис не будет знать и не будет заботиться о том, куда именно попадают эти логи.
Вы организуете свой код в трех файлах:
logger.go: Определите интерфейсLogger, который требует методLog(message string) string. Затем создайте две различные реализации логгера:ConsoleLoggerс полемPrefix— его методLogвозвращает[CONSOLE] [Prefix]: [message]FileLoggerс полемFilename— его методLogвозвращает[FILE:[Filename]] [message]
service.go: Создайте структуруAppService, которая зависит от интерфейсаLogger(а не от конкретного типа). Включите функцию-конструкторNewAppService, которая принимаетLoggerи возвращает указатель наAppService. ДобавьтеAppServiceметодDoWork(task string) string, который использует внедренный логгер для записи сообщенияProcessing: [task]и возвращает то, что возвращает логгер.main.go: Считайте конфигурацию из входных данных, создайте оба типа логгеров, внедрите каждый из них в отдельные экземплярыAppServiceи вызовитеDoWorkдля каждого сервиса с указанной задачей. Выведите результат каждого вызова сервиса.
Будут предоставлены следующие входные данные:
- Строка 1: Префикс консольного логгера
- Строка 2: Имя файла логгера
- Строка 3: Задача для обработки
Например, если даны INFO, app.log и user authentication, ваш вывод должен быть:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationОбратите внимание, что AppService не знает, записывает ли он данные в консоль или в файл — он просто вызывает Log у того логгера, который был внедрен. Эта гибкость и есть суть внедрения зависимостей: один и тот же код сервиса работает с совершенно разными реализациями логирования.
Шпаргалка
Внедрение зависимостей (Dependency Injection) — это метод, при котором структура получает свои зависимости извне, а не создает их внутри себя. В Go интерфейсы делают этот паттерн естественным и мощным.
Определите интерфейс для зависимости:
type Notifier interface {
Send(message string) string
}
Создайте конкретные реализации интерфейса:
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
Создайте структуру, которая зависит от интерфейса, а не от конкретного типа:
type OrderService struct {
notifier Notifier // зависимость внедряется через интерфейс
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
Внедрите зависимость при создании сервиса:
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Order placed: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Order placed: Phone
Этот подход делает код более гибким и тестируемым, позволяя заменять различные реализации без изменения кода самой структуры.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Чтение префикса консольного логгера
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// Чтение имени файла для файлового логгера
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// Чтение задачи для обработки
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO: Создать ConsoleLogger с указанным префиксом
// TODO: Создать FileLogger с указанным именем файла
// TODO: Создать AppService с внедренным ConsoleLogger
// TODO: Создать еще один AppService с внедренным FileLogger
// TODO: Вызвать DoWork для каждого сервиса с задачей и вывести результаты
fmt.Println("result1")
fmt.Println("result2")
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool