Внедрение зависимостей
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 45 из 107.
Внедрение зависимостей — это техника, при которой структура получает свои зависимости извне, а не создаёт их внутри. В 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
}
Теперь создай структуру, которая зависит от interface, а не от конкретного типа:
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 не знает и не заботится о том, использует ли он электронную почту или SMS. Вы внедряете зависимость при создании службы:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Заказ размещён: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Заказ размещён: Phone
}
Этот подход делает ваш код более гибким и тестируемым. Во время тестирования вы можете внедрить имитацию notifier, которая на самом деле не отправляет сообщения. В рабочей среде вы внедряете реальную реализацию. Структура остаётся неизменной в обоих случаях.
Задание
ЛегкоДавайте создадим систему ведения журналов, которая продемонстрирует возможности внедрения зависимостей. Вы создадите сервис, способный записывать журналы в разные места назначения, при этом сервису не нужно знать или важно, куда именно попадают эти журналы.
Вы организуете код в трёх файлах:
logger.go: Определите интерфейсLogger, который требует наличия методаLog(message string) string. Затем создайте две разные реализации logger:ConsoleLoggerс полемPrefix. Его методLogвозвращает[CONSOLE] [Prefix]: [message]FileLoggerс полемFilename. Его методLogвозвращает[FILE:[Filename]] [message]
service.go: Создайте структуруAppService, которая зависит от интерфейсаLogger(а не от конкретного типа). Добавьте функцию-конструкторNewAppService, которая принимаетLoggerи возвращает указатель наAppService. Добавьте вAppServiceметод с именемDoWork(task string) string, который использует injected logger для записи сообщенияProcessing: [task]и возвращает всё, что возвращает logger.main.go: Прочитайте конфигурацию из входных данных, создайте оба типа logger, внедрите каждый из них в отдельный экземплярAppServiceи вызовитеDoWorkдля каждого сервиса с переданной задачей. Выведите результат каждого вызова сервиса.
Будут предоставлены следующие входные данные:
- Строка 1: префикс Console logger
- Строка 2: имя файла File logger
- Строка 3: задача для обработки
Например, если заданы INFO, app.log и user authentication, результат должен быть следующим:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationОбратите внимание, что AppService не знает, ведётся ли журнал в консоль или в файл. Он просто вызывает Log у logger, который был injected. Эта гибкость и составляет суть внедрения зависимостей: один и тот же код сервиса работает с совершенно разными реализациями logger.
Попробуйте сами
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Потренируйтесь самостоятельно: Онлайн-компилятор Go