> For the complete documentation index, see [llms.txt](https://eda-1.gitbook.io/lgwt/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://eda-1.gitbook.io/lgwt/osnovy-go/dependency-injection.md).

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

[**Весь код для этой главы вы можете найти здесь**](https://github.com/quii/learn-go-with-tests/tree/main/di)

Предполагается, что вы уже прочитали [раздел о структурах](/lgwt/osnovy-go/structs-methods-and-interfaces.md), поскольку для этого потребуется некоторое понимание интерфейсов.

В программистском сообществе существует *множество* заблуждений относительно внедрения зависимостей. Надеемся, это руководство покажет вам, как:

* Вам не нужен фреймворк.
* Это не усложняет ваш дизайн.
* Это облегчает тестирование.
* Это позволяет вам писать отличные, универсальные функции.

Мы хотим написать функцию, которая приветствует кого-либо, точно так же, как мы это делали в главе о hello-world, но на этот раз мы будем тестировать *фактический вывод*.

Просто для напоминания, вот как могла бы выглядеть эта функция:

```go
func Greet(name string) {
	fmt.Printf("Hello, %s", name)
}
```

Но как мы можем это протестировать? Вызов `fmt.Printf` выводит данные в стандартный вывод (stdout), что довольно сложно для нас перехватить с помощью фреймворка для тестирования.

Нам нужно иметь возможность **внедрить** (что является просто причудливым словом для "передать в качестве аргумента") зависимость печати.

**Нашей функции не нужно беспокоиться о том,&#x20;*****где*****&#x20;или&#x20;*****как*****&#x20;происходит печать, поэтому мы должны принимать&#x20;*****интерфейс*****, а не конкретный тип.**

Если мы сделаем это, мы сможем изменить реализацию, чтобы печатать в нечто, что мы контролируем, для тестирования. В "реальной жизни" вы бы внедрили что-то, что записывает в `os.Stdout`.

Если вы посмотрите исходный код [`fmt.Printf`](https://pkg.go.dev/fmt#Printf), вы увидите способ, которым мы можем подключиться:

```go
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}
```

Интересно! Под капотом `Printf` просто вызывает `Fprintf`, передавая `os.Stdout`.

Что именно *такое* `os.Stdout`? Что `Fprintf` ожидает получить в качестве первого аргумента?

```go
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}
```

Интерфейс `io.Writer`:

```go
type Writer interface {
	Write(p []byte) (n int, err error)
}
```

Из этого мы можем заключить, что `os.Stdout` реализует `io.Writer`; `Printf` передает `os.Stdout` в `Fprintf`, который ожидает `io.Writer`.

По мере того, как вы будете писать больше Go-кода, вы обнаружите, что этот интерфейс появляется очень часто, потому что это отличный универсальный интерфейс для "поместить эти данные куда-либо".

Итак, мы знаем, что под капотом мы в конечном итоге используем `Writer` для отправки нашего приветствия куда-то. Давайте используем эту существующую абстракцию, чтобы сделать наш код тестируемым и более повторно используемым.

## Сначала напишите тест

```go
func TestGreet(t *testing.T) {
	buffer := bytes.Buffer{}
	Greet(&buffer, "Chris")

	got := buffer.String()
	want := "Hello, Chris"

	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}
```

Тип `Buffer` из пакета `bytes` реализует интерфейс `Writer`, потому что у него есть метод `Write(p []byte) (n int, err error)`.

Итак, мы будем использовать его в нашем тесте в качестве нашего `Writer`, а затем мы сможем проверить, что было записано в него после вызова `Greet`.

## Попробуйте запустить тест

Тест не скомпилируется:

```
./di_test.go:10:2: undefined: Greet
```

## Напишите минимальный объем кода, чтобы тест запустился, и проверьте вывод ошибочного теста

*Прислушайтесь к компилятору* и исправьте проблему.

```go
func Greet(writer *bytes.Buffer, name string) {
	fmt.Printf("Hello, %s", name)
}
```

`Hello, Chris di_test.go:16: got '' want 'Hello, Chris'`

Тест провалился. Обратите внимание, что имя выводится, но в стандартный вывод.

## Напишите достаточно кода, чтобы тест прошёл

Используйте `writer`, чтобы отправить приветствие в буфер в нашем тесте. Помните, что `fmt.Fprintf` похож на `fmt.Printf`, но вместо этого принимает `Writer` для отправки строки, тогда как `fmt.Printf` по умолчанию выводит в стандартный вывод.

```go
func Greet(writer *bytes.Buffer, name string) {
	fmt.Fprintf(writer, "Hello, %s", name)
}
```

Теперь тест проходит.

## Рефакторинг

Ранее компилятор сказал нам передать указатель на `bytes.Buffer`. Это технически верно, но не очень полезно.

Чтобы продемонстрировать это, попробуйте подключить функцию `Greet` к Go-приложению, где мы хотим, чтобы она печатала в стандартный вывод.

```go
func main() {
	Greet(os.Stdout, "Elodie")
}
```

`./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet`

Как обсуждалось ранее, `fmt.Fprintf` позволяет передать `io.Writer`, который, как мы знаем, реализуют как `os.Stdout`, так и `bytes.Buffer`.

Если мы изменим наш код для использования более универсального интерфейса, мы сможем использовать его как в тестах, так и в нашем приложении.

```go
package main

import (
	"fmt"
	"io"
	"os"
)

func Greet(writer io.Writer, name string) {
	fmt.Fprintf(writer, "Hello, %s", name)
}

func main() {
	Greet(os.Stdout, "Elodie")
}
```

## Подробнее об `io.Writer`

В какие еще места мы можем записывать данные с помощью `io.Writer`? Насколько универсальна наша функция `Greet`?

### Интернет

Запустите следующее:

```go
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func Greet(writer io.Writer, name string) {
	fmt.Fprintf(writer, "Hello, %s", name)
}

func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
	Greet(w, "world")
}

func main() {
	log.Fatal(http.ListenAndServe(":5001", http.HandlerFunc(MyGreeterHandler)))
}
```

Запустите программу и перейдите по ссылке <http://localhost:5001>. Вы увидите, как используется ваша функция приветствия.

HTTP-серверы будут рассмотрены в одной из следующих глав, поэтому не беспокойтесь слишком сильно о деталях.

Когда вы пишете HTTP-обработчик, вам предоставляются `http.ResponseWriter` и `http.Request`, которые использовались для выполнения запроса. При реализации вашего сервера вы *записываете* ваш ответ с помощью `writer`.

Вы, вероятно, можете догадаться, что `http.ResponseWriter` также реализует `io.Writer`, поэтому именно поэтому мы могли повторно использовать нашу функцию `Greet` внутри нашего обработчика.

## Подведение итогов

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

*Мотивированные нашими тестами*, мы провели рефакторинг кода, чтобы мы могли контролировать, *куда* записываются данные, **внедряя зависимость**, что позволило нам:

* **Тестировать наш код**. Если вы не можете протестировать функцию *легко*, это обычно происходит из-за зависимостей, жестко прописанных в функции, *или* глобального состояния. Например, если у вас есть глобальный пул подключений к базе данных, который используется каким-либо сервисным слоем, его, вероятно, будет сложно тестировать, и тесты будут медленными. Внедрение зависимостей (DI) побудит вас внедрить зависимость от базы данных (через интерфейс), которую вы затем сможете имитировать (mock) чем-то, что вы можете контролировать в своих тестах.
* **Разделять наши задачи**, отвязывая *куда идут данные* от *как их генерировать*. Если вам когда-либо кажется, что у метода/функции слишком много обязанностей (генерация данных *и* запись в базу данных? обработка HTTP-запросов *и* выполнение логики на уровне предметной области?), DI, вероятно, будет тем инструментом, который вам нужен.
* **Разрешить повторное использование нашего кода в различных контекстах**. Первый "новый" контекст, в котором может использоваться наш код, — это тесты. Но далее, если кто-то захочет попробовать что-то новое с вашей функцией, он может внедрить свои собственные зависимости.

### А как насчет мокирования? Я слышал, что оно нужно для DI, и оно также "зло"

Мокирование будет подробно рассмотрено позже (и оно не является "злом"). Вы используете мокирование, чтобы заменить реальные объекты, которые вы внедряете, имитируемой версией, которую вы можете контролировать и проверять в своих тестах. В нашем случае, однако, стандартная библиотека уже имела что-то готовое для использования.

### Стандартная библиотека Go действительно хороша, уделите время ее изучению

Имея некоторое знакомство с интерфейсом `io.Writer`, мы смогли использовать `bytes.Buffer` в нашем тесте в качестве нашего `Writer`, а затем мы можем использовать другие `Writer`ы из стандартной библиотеки для использования нашей функции в приложении командной строки или в веб-сервере.

Чем лучше вы знакомы со стандартной библиотекой, тем чаще вы будете видеть эти универсальные интерфейсы, которые затем сможете повторно использовать в своем собственном коде, чтобы сделать ваше программное обеспечение многократно используемым в различных контекстах.

## Этот пример сильно вдохновлен главой из книги [The Go Programming Language](https://www.amazon.co.uk/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440), так что если вам понравилось, купите ее!


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://eda-1.gitbook.io/lgwt/osnovy-go/dependency-injection.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
