Menu
Coddy logo textTech

Внедрение зависимостей

Часть раздела Объектно-ориентированное программирование путешествия по 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, которая на самом деле не отправляет сообщения. В рабочей среде вы внедряете реальную реализацию. Структура остаётся неизменной в обоих случаях.

challenge icon

Задание

Легко

Давайте создадим систему ведения журналов, которая продемонстрирует возможности внедрения зависимостей. Вы создадите сервис, способный записывать журналы в разные места назначения, при этом сервису не нужно знать или важно, куда именно попадают эти журналы.

Вы организуете код в трёх файлах:

  • 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")
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор Go