Nette Documentation Preview

syntax
Что такое «внедрение зависимостей»?
***********************************

.[perex]
Эта глава знакомит вас с основными практиками программирования, которым вы должны следовать при написании любого приложения. Это основы, необходимые для написания чистого, понятного и сопровождаемого кода.

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

Принципы, которые мы здесь покажем, довольно просты. Вам не о чем беспокоиться.


Помните свою первую программу? .[#toc-remember-your-first-program]
------------------------------------------------------------------

Мы понятия не имеем, на каком языке вы её написали, но если бы это был PHP, она, вероятно, выглядела бы примерно так:

```php
function addition(float $a, float $b): float
{
	return $a + $b;
}

echo addition(23, 1); // выводит 24
```

Несколько тривиальных строк кода, но в них скрыто так много ключевых понятий. Мы видим, что есть переменные. Что код разбивается на более мелкие единицы, которыми являются, например, функции. Что мы передаем им входные аргументы, а они возвращают результаты. Всё, чего не хватает — это условия и циклы.

То, что мы передаем функции входные данные, а она возвращает результат — это вполне понятная концепция, которая используется и в других областях, например, в математике.

Функция имеет сигнатуру, которая состоит из её имени, списка параметров и их типов, и, наконец, типа возвращаемого значения. Как пользователей, нас интересует сигнатура; нам обычно не нужно знать ничего о внутренней реализации.

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

```php
function addition(float $x): float
```

Дополнение с одним параметром? Странно... Как насчет этого?

```php
function addition(): float
```

Это очень странно, не так ли? Как вы думаете, как используется эта функция?

```php
echo addition(); // что здесь выводится?
```

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

Интересно, как такая функция будет выглядеть внутри? Откуда она возьмет переменные? Вероятно, она могла бы получить их *каким-то образом* самостоятельно, вот так:

```php
function addition(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}
```

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


Не так! .[#toc-not-this-way]
----------------------------

Дизайн, который мы только что показали, является сущностью многих отрицательных признаков:

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

А входит ли вообще в задачу функции сложения получение исходных данных? Конечно, нет.  В её обязанности входит только сложение.


Мы не хотим сталкиваться с таким кодом, и уж точно не хотим его писать. Решение простое: вернитесь к основам и просто используйте параметры:


```php
function addition(float $a, float $b): float
{
	return $a + $b;
}
```


Правило №1: Пусть вам передадут .[#toc-rule-1-let-it-be-passed-to-you]
----------------------------------------------------------------------

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

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

Если вы будете следовать этому правилу всегда и везде, вы на пути к коду без скрытых привязок. К коду, который понятен не только автору, но и любому, кто его потом прочитает. Где все понятно из сигнатур функций и классов и нет необходимости искать скрытые секреты в реализации.

Эта техника мастерски называется **инъекция зависимости**. А данные называются **зависимости**. Но это простая передача параметров, не более того.

.[note]
Пожалуйста, не путайте инъекцию зависимостей, которая является паттерном проектирования, с "контейнером инъекции зависимостей", который является инструментом, совершенно другим. О контейнерах мы поговорим позже.


От функций к классам .[#toc-from-functions-to-classes]
------------------------------------------------------

А как классы связаны с этим? Класс — это более сложная сущность, чем простая функция, но и здесь действует правило №1. Просто есть [больше способов передачи аргументов|passing-dependencies]. Например, очень похоже на случай с функцией:

```php
class Math
{
	public function addition(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Math;
echo $math->addition(23, 1); // 24
```

Или с помощью других методов, или при помощи конструктора:

```php
class Addition
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function calculate(): float
	{
		return $this->a + $this->b;
	}

}

$addition = new Addition(23, 1);
echo $addition->calculate(); // 24
```

Оба примера полностью соответствуют принципу внедрения зависимостей.


Примеры из реальной жизни .[#toc-real-life-examples]
----------------------------------------------------

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

Пусть у нас есть класс `Article`, представляющий статью в блоге:

```php
class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		// сохранить статью в базе данных
	}
}
```

а его использование будет следующим:

```php
$article = new Article;
$article->title = '10 вещей, которые нужно знать о потере веса';
$article->content = 'Каждый год миллионы людей в ...';
$article->save();
```

Метод `save()` сохранит статью в таблице базы данных. Реализация этого метода с использованием [Nette Database |database:] была бы простым делом, если бы не одна загвоздка: где `Article` получает соединение с базой данных, т. е. объект класса `Nette\Database\Connection`?

Кажется, у нас есть много вариантов. Он может взять его из какой-то статической переменной. Или наследоваться от класса, который будет предоставлять соединение с базой данных. Или воспользоваться так называемым синглтоном. Или воспользоваться так называемыми фасадами, которые используются в Laravel:

```php
use Illuminate\Support\Facades\DB;

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}
```

Отлично, мы решили проблему.

Или нет?

Давайте вспомним [правило №1: Пусть вам передадут |#rule #1: Let It Be Passed to You]: все зависимости, которые нужны классу, должны быть переданы ему. Потому что если мы этого не сделаем и нарушим правило, мы начнем путь к грязному коду, полному скрытых привязок, непонятности, и в результате получим приложение, которое будет больно поддерживать и развивать.

Пользователь класса `Article` понятия не имеет, где метод `save()` хранит статью. В таблице базы данных? В какой, в производственной или в разработке? И как это можно изменить?

Пользователь должен посмотреть, как реализован метод `save()`, чтобы найти использование метода `DB::insert()`. Поэтому ему приходится искать дальше, чтобы выяснить, как этот метод обеспечивает подключение к базе данных. А скрытые привязки могут образовывать довольно длинную цепочку.

Скрытые привязки, фасады Laravel или статические переменные никогда не присутствуют в чистом, хорошо продуманном коде. В чистом и хорошо продуманном коде передаются аргументы:

```php
class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}
```

Еще более практичным, как мы увидим дальше, является использование конструктора:

```php
class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}
```

.[note]
Если вы опытный программист, вы можете подумать, что у `Article` вообще не должно быть метода `save()`, это должен быть чистый компонент данных, а о хранении данных должен позаботиться отдельный репозиторий. В этом есть смысл. Но это выведет нас далеко за рамки темы - инъекции зависимостей - и попыток привести простые примеры.

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

Как насчет класса, который регистрирует сообщения об ошибках:

```php
class Logger
{
	public function log(string $message)
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}
```

Как вы думаете, мы выполнили [правило №1: Пусть вам передадут |#rule #1: Let It Be Passed to You]?

Нет.

Класс *получает* ключевую информацию, директорию, содержащую файл журнала, из константы.

Посмотрите пример использования:

```php
$logger = new Logger;
$logger->log('Температура 23 °C');
$logger->log('Температура 10 °C');
```

Не зная реализации, можете ли вы ответить на вопрос, где записаны сообщения? Не кажется ли вам, что существование константы LOG_DIR необходимо для его работы? И сможете ли вы создать второй экземпляр, который будет писать в другое место? Конечно, нет.

Давайте исправим класс:

```php
class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}
```

Теперь класс стал намного понятнее, более настраиваемым и, следовательно, более полезным:

```php
$logger = new Logger('/path/to/log.txt');
$logger->log('Температура 15 °C');
```


Но мне всё равно! .[#toc-but-i-don-t-care]
------------------------------------------

*«Когда я создаю объект Article и вызываю save(), я не хочу иметь дело с базой данных, я просто хочу, чтобы он был сохранен в той, которую я установил в конфигурации.»*

*«Когда я использую Logger, я просто хочу, чтобы сообщение было записано, и я не хочу разбираться с тем, куда. Пусть используются глобальные настройки.»*

Это правильные комментарии.

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

```php
class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Были разосланы электронные письма');

		} catch (Exception $e) {
			$logger->log('Во время отправки произошла ошибка');
			throw $e;
		}
	}
}
```

Улучшенный `Logger`, который больше не использует константу `LOG_DIR`, требует указания пути к файлу в конструкторе. Как решить эту проблему? Классу `NewsletterDistributor` все равно, куда записываются сообщения, он просто хочет их записать.

Решением снова является [правило №1: Пусть вам передадут |#rule #1: Let It Be Passed to You]: передавайте классу все данные, которые ему нужны.

Поэтому мы передаем путь к журналу в конструктор, который затем используем для создания объекта `Logger`?

```php
class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ НЕ ТАК!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);
```

Не так! Потому что путь **не** принадлежит к данным, которые нужны классу `NewsletterDistributor`; ему нужен `Logger`. Классу нужен сам логгер. И именно его мы и передадим:

```php
class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Были разосланы электронные письма');

		} catch (Exception $e) {
			$this->logger->log('Во время отправки произошла ошибка');
			throw $e;
		}
	}
}
```

Теперь из сигнатур класса `NewsletterDistributor` ясно, что ведение журнала является частью его функциональности. И задача замены логгера на другой, возможно, в целях тестирования, достаточно тривиальна.
Более того, если конструктор класса `Logger` будет изменен, это никак не повлияет на наш класс.


Правило №2: Берите то, что принадлежит вам .[#toc-rule-2-take-what-is-yours]
----------------------------------------------------------------------------

Не вводите себя в заблуждение и не позволяйте передавать вам параметры зависимостей. Передавайте зависимости напрямую.

Это сделает код, использующий другие объекты, полностью независимым от изменений в их конструкторах. Его API будет более правильным. И самое главное, будет тривиально поменять эти зависимости на другие.


Новый член семьи .[#toc-a-new-member-of-the-family]
---------------------------------------------------

Команда разработчиков решила создать второй регистратор, который пишет в базу данных. Поэтому мы создаем класс `DatabaseLogger`. Итак, у нас есть два класса, `Logger` и `DatabaseLogger`, один пишет в файл, другой - в базу данных... не кажется ли вам, что в этом названии есть что-то странное?
Не лучше ли переименовать `Logger` в `FileLogger`? Конечно, лучше.

Но давайте сделаем это по-умному. Мы создадим интерфейс под оригинальным именем:

```php
interface Logger
{
	function log(string $message): void;
}
```

...который будут реализовывать оба регистратора:

```php
class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...
```

И таким образом, ничего не нужно будет менять в остальной части кода, где используется логгер. Например, конструктор класса `NewsletterDistributor` по-прежнему будет рад потребовать `Logger` в качестве параметра. И только от нас будет зависеть, какой экземпляр мы ему передадим.

**Вот почему мы никогда не даем именам интерфейсов суффикс `Interface` или префикс `I`.** Иначе было бы невозможно разработать такой красивый код.


Хьюстон, у нас проблема .[#toc-houston-we-have-a-problem]
---------------------------------------------------------

Если во всем приложении мы можем быть довольны одним экземпляром регистратора, будь то файл или база данных, и просто передавать его везде, где что-то регистрируется, то в случае с классом `Article` все обстоит совсем иначе. Фактически, мы создаем его экземпляры по мере необходимости, возможно, несколько раз. Как быть с привязкой к базе данных в его конструкторе?

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

```php
class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}
```

Возможное решение предлагается напрямую: пусть объект базы данных передается конструктором в `EditController` и используется `$article = new Article($this->db)`.

Как и в предыдущем случае с `Logger` и путем к файлу, это неправильный подход. База данных является зависимостью не от `EditController`, а от `Article`. Поэтому передача базы данных противоречит [правилу #2: бери то, что принадлежит тебе |#rule #2: take what is yours]. Когда конструктор класса `Article` будет изменен (добавлен новый параметр), код во всех местах, где создаются экземпляры, также должен быть изменен. Уффф.

Хьюстон, что вы предлагаете?


Правило №3: Пусть завод сам разбирается с этим .[#toc-rule-3-let-the-factory-handle-it]
---------------------------------------------------------------------------------------

Убрав скрытые привязки и передавая все зависимости в качестве аргументов, мы получаем более настраиваемые и гибкие классы. И поэтому нам нужно что-то еще для создания и настройки этих более гибких классов. Мы назовем это фабриками.

Эмпирическое правило таково: если класс имеет зависимости, оставьте создание их экземпляров фабрике.

Фабрики являются более разумной заменой оператору `new` в мире внедрения зависимостей.

.[note]
Пожалуйста, не путайте с шаблоном проектирования *factory method*, который описывает конкретный способ использования фабрик и не имеет отношения к данной теме.


Фабрика .[#toc-factory]
-----------------------

Фабрика - это метод или класс, который производит и настраивает объекты. Мы называем `Article` производящий класс `ArticleFactory`, и он может выглядеть следующим образом:
```php
class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}
```

Его использование в контроллере будет выглядеть следующим образом:

```php
class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// позволить фабрике создать объект
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}
```

На данный момент, когда сигнатура конструктора класса `Article` изменяется, единственная часть кода, которая должна реагировать, это сама фабрика `ArticleFactory`. Любой другой код, работающий с объектами `Article`, например `EditController`, не будет затронут.

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

Не волнуйтесь, скоро мы перейдем к контейнеру Nette DI. А у него есть несколько тузов в рукаве, которые сделают создание приложений с использованием инъекции зависимостей чрезвычайно простым. Например, вместо класса `ArticleFactory` достаточно [написать простой интерфейс |factory]:

```php
interface ArticleFactory
{
	function create(): Article;
}
```

Но мы забегаем вперед, подождите :-)


Резюме .[#toc-summary]
----------------------

В начале этой главы мы обещали показать вам способ разработки чистого кода. Просто дайте классам

- [те зависимости, которые им нужны |#Rule #1: Let It Be Passed to You]
- [а не то, что им напрямую не нужно |#Rule #2: Take What Is Yours]
- [и что объекты с зависимостями лучше всего создавать в фабриках |#Rule #3: Let the Factory Handle it]

На первый взгляд может показаться, что это не так, но эти три правила имеют далеко идущие последствия. Они приводят к радикально иному взгляду на проектирование кода. Стоит ли оно того? Программисты, которые отбросили старые привычки и начали последовательно использовать внедрение зависимостей, считают это поворотным моментом в своей профессиональной жизни. Он открыл мир понятных и устойчивых приложений.

Но что, если в коде не используется последовательное внедрение зависимостей? Что если он построен на статических методах или синглтонах? Приносит ли это какие-либо проблемы? Приносит [, и очень существенные |global-state].

Что такое «внедрение зависимостей»?

Эта глава знакомит вас с основными практиками программирования, которым вы должны следовать при написании любого приложения. Это основы, необходимые для написания чистого, понятного и сопровождаемого кода.

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

Принципы, которые мы здесь покажем, довольно просты. Вам не о чем беспокоиться.

Помните свою первую программу?

Мы понятия не имеем, на каком языке вы её написали, но если бы это был PHP, она, вероятно, выглядела бы примерно так:

function addition(float $a, float $b): float
{
	return $a + $b;
}

echo addition(23, 1); // выводит 24

Несколько тривиальных строк кода, но в них скрыто так много ключевых понятий. Мы видим, что есть переменные. Что код разбивается на более мелкие единицы, которыми являются, например, функции. Что мы передаем им входные аргументы, а они возвращают результаты. Всё, чего не хватает — это условия и циклы.

То, что мы передаем функции входные данные, а она возвращает результат — это вполне понятная концепция, которая используется и в других областях, например, в математике.

Функция имеет сигнатуру, которая состоит из её имени, списка параметров и их типов, и, наконец, типа возвращаемого значения. Как пользователей, нас интересует сигнатура; нам обычно не нужно знать ничего о внутренней реализации.

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

function addition(float $x): float

Дополнение с одним параметром? Странно… Как насчет этого?

function addition(): float

Это очень странно, не так ли? Как вы думаете, как используется эта функция?

echo addition(); // что здесь выводится?

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

Интересно, как такая функция будет выглядеть внутри? Откуда она возьмет переменные? Вероятно, она могла бы получить их каким-то образом самостоятельно, вот так:

function addition(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

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

Не так!

Дизайн, который мы только что показали, является сущностью многих отрицательных признаков:

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

А входит ли вообще в задачу функции сложения получение исходных данных? Конечно, нет. В её обязанности входит только сложение.

Мы не хотим сталкиваться с таким кодом, и уж точно не хотим его писать. Решение простое: вернитесь к основам и просто используйте параметры:

function addition(float $a, float $b): float
{
	return $a + $b;
}

Правило №1: Пусть вам передадут

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

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

Если вы будете следовать этому правилу всегда и везде, вы на пути к коду без скрытых привязок. К коду, который понятен не только автору, но и любому, кто его потом прочитает. Где все понятно из сигнатур функций и классов и нет необходимости искать скрытые секреты в реализации.

Эта техника мастерски называется инъекция зависимости. А данные называются зависимости. Но это простая передача параметров, не более того.

Пожалуйста, не путайте инъекцию зависимостей, которая является паттерном проектирования, с „контейнером инъекции зависимостей“, который является инструментом, совершенно другим. О контейнерах мы поговорим позже.

От функций к классам

А как классы связаны с этим? Класс — это более сложная сущность, чем простая функция, но и здесь действует правило №1. Просто есть больше способов передачи аргументов. Например, очень похоже на случай с функцией:

class Math
{
	public function addition(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Math;
echo $math->addition(23, 1); // 24

Или с помощью других методов, или при помощи конструктора:

class Addition
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function calculate(): float
	{
		return $this->a + $this->b;
	}

}

$addition = new Addition(23, 1);
echo $addition->calculate(); // 24

Оба примера полностью соответствуют принципу внедрения зависимостей.

Примеры из реальной жизни

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

Пусть у нас есть класс Article, представляющий статью в блоге:

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		// сохранить статью в базе данных
	}
}

а его использование будет следующим:

$article = new Article;
$article->title = '10 вещей, которые нужно знать о потере веса';
$article->content = 'Каждый год миллионы людей в ...';
$article->save();

Метод save() сохранит статью в таблице базы данных. Реализация этого метода с использованием Nette Database была бы простым делом, если бы не одна загвоздка: где Article получает соединение с базой данных, т. е. объект класса Nette\Database\Connection?

Кажется, у нас есть много вариантов. Он может взять его из какой-то статической переменной. Или наследоваться от класса, который будет предоставлять соединение с базой данных. Или воспользоваться так называемым синглтоном. Или воспользоваться так называемыми фасадами, которые используются в Laravel:

use Illuminate\Support\Facades\DB;

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}

Отлично, мы решили проблему.

Или нет?

Давайте вспомним правило №1: Пусть вам передадут: все зависимости, которые нужны классу, должны быть переданы ему. Потому что если мы этого не сделаем и нарушим правило, мы начнем путь к грязному коду, полному скрытых привязок, непонятности, и в результате получим приложение, которое будет больно поддерживать и развивать.

Пользователь класса Article понятия не имеет, где метод save() хранит статью. В таблице базы данных? В какой, в производственной или в разработке? И как это можно изменить?

Пользователь должен посмотреть, как реализован метод save(), чтобы найти использование метода DB::insert(). Поэтому ему приходится искать дальше, чтобы выяснить, как этот метод обеспечивает подключение к базе данных. А скрытые привязки могут образовывать довольно длинную цепочку.

Скрытые привязки, фасады Laravel или статические переменные никогда не присутствуют в чистом, хорошо продуманном коде. В чистом и хорошо продуманном коде передаются аргументы:

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Еще более практичным, как мы увидим дальше, является использование конструктора:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Если вы опытный программист, вы можете подумать, что у Article вообще не должно быть метода save(), это должен быть чистый компонент данных, а о хранении данных должен позаботиться отдельный репозиторий. В этом есть смысл. Но это выведет нас далеко за рамки темы – инъекции зависимостей – и попыток привести простые примеры.

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

Как насчет класса, который регистрирует сообщения об ошибках:

class Logger
{
	public function log(string $message)
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}

Как вы думаете, мы выполнили правило №1: Пусть вам передадут?

Нет.

Класс получает ключевую информацию, директорию, содержащую файл журнала, из константы.

Посмотрите пример использования:

$logger = new Logger;
$logger->log('Температура 23 °C');
$logger->log('Температура 10 °C');

Не зная реализации, можете ли вы ответить на вопрос, где записаны сообщения? Не кажется ли вам, что существование константы LOG_DIR необходимо для его работы? И сможете ли вы создать второй экземпляр, который будет писать в другое место? Конечно, нет.

Давайте исправим класс:

class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

Теперь класс стал намного понятнее, более настраиваемым и, следовательно, более полезным:

$logger = new Logger('/path/to/log.txt');
$logger->log('Температура 15 °C');

Но мне всё равно!

«Когда я создаю объект Article и вызываю save(), я не хочу иметь дело с базой данных, я просто хочу, чтобы он был сохранен в той, которую я установил в конфигурации.»

«Когда я использую Logger, я просто хочу, чтобы сообщение было записано, и я не хочу разбираться с тем, куда. Пусть используются глобальные настройки.»

Это правильные комментарии.

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

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Были разосланы электронные письма');

		} catch (Exception $e) {
			$logger->log('Во время отправки произошла ошибка');
			throw $e;
		}
	}
}

Улучшенный Logger, который больше не использует константу LOG_DIR, требует указания пути к файлу в конструкторе. Как решить эту проблему? Классу NewsletterDistributor все равно, куда записываются сообщения, он просто хочет их записать.

Решением снова является правило №1: Пусть вам передадут: передавайте классу все данные, которые ему нужны.

Поэтому мы передаем путь к журналу в конструктор, который затем используем для создания объекта Logger?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ НЕ ТАК!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

Не так! Потому что путь не принадлежит к данным, которые нужны классу NewsletterDistributor; ему нужен Logger. Классу нужен сам логгер. И именно его мы и передадим:

class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Были разосланы электронные письма');

		} catch (Exception $e) {
			$this->logger->log('Во время отправки произошла ошибка');
			throw $e;
		}
	}
}

Теперь из сигнатур класса NewsletterDistributor ясно, что ведение журнала является частью его функциональности. И задача замены логгера на другой, возможно, в целях тестирования, достаточно тривиальна. Более того, если конструктор класса Logger будет изменен, это никак не повлияет на наш класс.

Правило №2: Берите то, что принадлежит вам

Не вводите себя в заблуждение и не позволяйте передавать вам параметры зависимостей. Передавайте зависимости напрямую.

Это сделает код, использующий другие объекты, полностью независимым от изменений в их конструкторах. Его API будет более правильным. И самое главное, будет тривиально поменять эти зависимости на другие.

Новый член семьи

Команда разработчиков решила создать второй регистратор, который пишет в базу данных. Поэтому мы создаем класс DatabaseLogger. Итак, у нас есть два класса, Logger и DatabaseLogger, один пишет в файл, другой – в базу данных… не кажется ли вам, что в этом названии есть что-то странное? Не лучше ли переименовать Logger в FileLogger? Конечно, лучше.

Но давайте сделаем это по-умному. Мы создадим интерфейс под оригинальным именем:

interface Logger
{
	function log(string $message): void;
}

…который будут реализовывать оба регистратора:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

И таким образом, ничего не нужно будет менять в остальной части кода, где используется логгер. Например, конструктор класса NewsletterDistributor по-прежнему будет рад потребовать Logger в качестве параметра. И только от нас будет зависеть, какой экземпляр мы ему передадим.

Вот почему мы никогда не даем именам интерфейсов суффикс Interface или префикс I. Иначе было бы невозможно разработать такой красивый код.

Хьюстон, у нас проблема

Если во всем приложении мы можем быть довольны одним экземпляром регистратора, будь то файл или база данных, и просто передавать его везде, где что-то регистрируется, то в случае с классом Article все обстоит совсем иначе. Фактически, мы создаем его экземпляры по мере необходимости, возможно, несколько раз. Как быть с привязкой к базе данных в его конструкторе?

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

class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Возможное решение предлагается напрямую: пусть объект базы данных передается конструктором в EditController и используется $article = new Article($this->db).

Как и в предыдущем случае с Logger и путем к файлу, это неправильный подход. База данных является зависимостью не от EditController, а от Article. Поэтому передача базы данных противоречит правилу #2: бери то, что принадлежит тебе. Когда конструктор класса Article будет изменен (добавлен новый параметр), код во всех местах, где создаются экземпляры, также должен быть изменен. Уффф.

Хьюстон, что вы предлагаете?

Правило №3: Пусть завод сам разбирается с этим

Убрав скрытые привязки и передавая все зависимости в качестве аргументов, мы получаем более настраиваемые и гибкие классы. И поэтому нам нужно что-то еще для создания и настройки этих более гибких классов. Мы назовем это фабриками.

Эмпирическое правило таково: если класс имеет зависимости, оставьте создание их экземпляров фабрике.

Фабрики являются более разумной заменой оператору new в мире внедрения зависимостей.

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

Фабрика

Фабрика – это метод или класс, который производит и настраивает объекты. Мы называем Article производящий класс ArticleFactory, и он может выглядеть следующим образом:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Его использование в контроллере будет выглядеть следующим образом:

class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// позволить фабрике создать объект
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

На данный момент, когда сигнатура конструктора класса Article изменяется, единственная часть кода, которая должна реагировать, это сама фабрика ArticleFactory. Любой другой код, работающий с объектами Article, например EditController, не будет затронут.

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

Не волнуйтесь, скоро мы перейдем к контейнеру Nette DI. А у него есть несколько тузов в рукаве, которые сделают создание приложений с использованием инъекции зависимостей чрезвычайно простым. Например, вместо класса ArticleFactory достаточно написать простой интерфейс:

interface ArticleFactory
{
	function create(): Article;
}

Но мы забегаем вперед, подождите :-)

Резюме

В начале этой главы мы обещали показать вам способ разработки чистого кода. Просто дайте классам

На первый взгляд может показаться, что это не так, но эти три правила имеют далеко идущие последствия. Они приводят к радикально иному взгляду на проектирование кода. Стоит ли оно того? Программисты, которые отбросили старые привычки и начали последовательно использовать внедрение зависимостей, считают это поворотным моментом в своей профессиональной жизни. Он открыл мир понятных и устойчивых приложений.

Но что, если в коде не используется последовательное внедрение зависимостей? Что если он построен на статических методах или синглтонах? Приносит ли это какие-либо проблемы? Приносит , и очень существенные.