diff --git a/manuscript/2-di.md b/manuscript/2-di.md index 449ca38..83b12f4 100644 --- a/manuscript/2-di.md +++ b/manuscript/2-di.md @@ -549,12 +549,12 @@ open class Foo {} Мне нравится это. -Эта концепция 'final or abstract' не полностью устраняет опасность наследования реализации. -Абстрактный класс с protected методами и его наследники могут попасть в такую же ситуацию, как описанную ранее с очередями. +Эта концепция — «final or abstract» — не полностью устраняет опасность наследования реализации. +Абстрактный класс с protected-методами и его наследники могут попасть в такую же ситуацию, как описанную ранее с очередями. Каждое использование ключевого слова protected создаёт неявную связь между классами предка и наследника. Изменения в классе могут породить баги в наследниках. -Механизм Внедрения Зависимостей предоставляет возможность просто просить нужный функционал, без необходимости наследоваться. +Механизм внедрения зависимостей предоставляет возможность просто просить нужный функционал, без необходимости наследоваться. Задача логирования сообщений в очереди может быть решена с помощью шаблона Декоратор: ```php @@ -589,16 +589,16 @@ $this->app->when(LoggingQueue::class) ->give(RedisQueue::class); ``` -Предупреждение: этот код не будет работать в реальном Laravel окружении, поскольку эти классы имеют более сложную процедуру инициации. +Предупреждение: этот код не будет работать в реальном Laravel-окружении, поскольку эти классы имеют более сложную процедуру инициации. Контейнер с такой конфигурацией будет подставлять экземпляр **LoggingQueue** каждому, кто просит экземпляр **Queue**. Экземпляры же класса **LoggingQueue** будут получать экземпляр **RedisQueue** и будут предоставлять такой же функционал. После обновления Laravel с новым методом **pushOn** появится ошибка, что класс **LoggingQueue** не реализует все методы интерфейса **Queue**. Команда может решить как именно логировать этот метод и нужно ли. Еще одним плюсом данного подхода является то, что конструктор классов полностью под контролем. -В варианте с наследованием приходится вызывать parent::__construct и передавать туда все нужные параметры. +В варианте с наследованием приходится вызывать `parent::__construct` и передавать туда все нужные параметры. Это станет дополнительной, совершенно ненужной связью между двумя классами. -Класс декоратора же не имеет никаких неявных связей с декорируемым классом и позволяет избежать целый ряд проблем в будущем. +Класс декоратора же не имеет никаких неявных связей с декорируемым классом и позволяет избежать целого ряда проблем в будущем. ## Пример с загрузкой картинок @@ -608,7 +608,7 @@ $this->app->when(LoggingQueue::class) * загружаемая картинка должна быть проверена на неприемлемый контент * если проверка пройдена, картинка должна быть загружена в определенную папку -* если нет, то пользователь, кто загрузил эту картинку должен быть заблокирован после нескольких попыток +* если нет, то пользователь, который загрузил эту картинку, должен быть заблокирован после нескольких попыток. ```php final class ImageUploader @@ -647,9 +647,9 @@ final class ImageUploader // Проверки используя $this->googleVision, // $weakerRules и $fileContent - if(check failed) - if(!$dontBan) { - if(\RateLimiter::..., $banThreshold)) { + if (check failed) + if (!$dontBan) { + if (\RateLimiter::..., $banThreshold)) { $this->banUser(\Auth::user()); } } @@ -682,11 +682,11 @@ final class ImageUploader Если представить, что класс **ImageUploader** будет вызываться из консоли, то **\Auth::user()** будет возвращать null, поэтому должна быть добавлена соответствующая проверка, но гораздо удобнее и гибче просто просить в этом методе объект пользователя (**User** $uploadedBy) потому, что: 1. В этом случае можно быть уверенным, что в этой переменной будет не-null значение. -2. Каждый вызывающий этот класс может сам решить какой объект пользователя ему передать. Это не всегда **\Auth::user()**. +2. Каждый, кто вызывает этот класс, может сам решить какой объект пользователя ему передать. Это не всегда **\Auth::user()**. Функционал блокировки пользователя может понадобиться где-то еще. Сейчас это всего две строки кода, но в будущем там могут появиться отправка email и другие действия. -Выделим отдельный класс для этого. +Выделим отдельный класс для этого: ```php final class BanUserCommand @@ -699,16 +699,13 @@ final class BanUserCommand } ``` -> Это действие часто встречает мощное противодействие со стороны других разработчиков. "Зачем делать целый класс ради двух строк?" -> "Теперь будет трудно читать код, ведь надо будет каждый раз искать этот новый класс в редакторе, чтобы посмотреть как все сделано". -> В книге потихоньку будут даваться частичные причины такого выноса логики в классы. Здесь же я могу лишь написать, что в современных IDE классы создаются за секунды, -> навигация осуществляется одним кликом, а название класса **BanUserCommand** быстро позволяет осознать, что он делает, без заглядывания внутрь. +> Это действие часто встречает мощное противодействие со стороны других разработчиков. «Зачем делать целый класс ради двух строк?». +> «Теперь будет трудно читать код, ведь надо будет каждый раз искать этот новый класс в редакторе, чтобы посмотреть как все сделано». +> В книге потихоньку будут даваться частичные причины такого выноса логики в классы. Здесь же я могу лишь написать, что в современных IDE классы создаются за секунды, навигация осуществляется одним кликом, а название класса **BanUserCommand** быстро позволяет понять, что он делает, без заглядывания внутрь. -Следующая ответственность: "блокировка пользователя после нескольких попыток загрузить неподобающий контент". +Следующая ответственность: «блокировка пользователя после нескольких попыток загрузить неподобающий контент». Параметр **$banThreshold** был добавлен в попытке добавить гибкости классу. Как часто случается, эта гибкость никому не оказалась нужной. -Стандартное значение 5 всех устраивало. -Проще это вынести в константу. -Если в будущем эта гибкость понадобится, можно будет это добавить через конфигурацию или параметры фабрики. +Стандартное значение 5 всех устраивало. Проще это вынести в константу. Если в будущем эта гибкость понадобится, можно будет это добавить через конфигурацию или параметры фабрики. ```php final class WrongImageUploadsListener @@ -734,9 +731,10 @@ final class WrongImageUploadsListener $rateLimiterResult = $this->rateLimiter ->tooManyAttempts( 'user_wrong_image_uploads_' . $user->id, - self::BAN_THRESHOLD); + self::BAN_THRESHOLD + ); - if($rateLimiterResult) { + if ($rateLimiterResult) { $this->banUserCommand->banUser($user); return false; } @@ -747,7 +745,7 @@ final class WrongImageUploadsListener Реакция системы на загрузку неподобающего контента может поменяться в будущем, но эти изменения коснутся только этого класса. Эта локальность изменений, когда для изменения одной логики не надо копаться в тонне другой, крайне важна для больших проектов. -Следующая ответственность, которую надо убрать, это "проверка контента картинок": +Следующая ответственность, которую надо убрать, это «проверка контента картинок»: ```php final class ImageGuard @@ -815,8 +813,8 @@ final class ImageUploader { $fileContent = $file->getContents(); - if(!$this->imageGuard->check($fileContent, $weakerRules)) { - if(!$dontBan) { + if (!$this->imageGuard->check($fileContent, $weakerRules)) { + if (!$dontBan) { $this->listener->handle($uploadedBy); } @@ -836,9 +834,8 @@ final class ImageUploader Класс **ImageUploader** потерял несколько ответственностей и весьма рад этому. Он не заботится о том, как именно проверять картинки и что произойдёт, если там будет нарисовано что-то нехорошее. -Он просто выполняет некую оркестрацию. -Но мне все еще не нравятся параметры метода **upload**. -Ответственности были вынесены в соответствующие классы, но их параметры все еще здесь и вызовы этого метода до сих пор выглядят уродливо: +Он просто выполняет некую оркестрацию. Но мне все еще не нравятся параметры метода **upload**. +Ответственности были вынесены в соответствующие классы, но их параметры все ещё здесь и вызовы этого метода до сих пор выглядят уродливо: ```php $imageUploader->upload($file, $user, 'gallery', false, true); @@ -846,8 +843,8 @@ $imageUploader->upload($file, $user, 'gallery', false, true); Булевы параметры всегда выглядят уродливо и повышают когнитивную нагрузку на чтение кода. Я попробую удалить их двумя разными путями: -* ООП путем -* Путем конфигурации +* ООП путем; +* путём конфигурации. ### ООП путь @@ -990,17 +987,15 @@ final class ImageUploader ``` Логика булевых параметров переехала в интерфейсы и их реализации. -Работа с файловой системой тоже может быть упрощена созданием фасада для работы с ней (я говорю о шаблоне **Facade**, а не о Laravel фасадах). +Работа с файловой системой тоже может быть упрощена созданием фасада для работы с ней (я говорю о шаблоне **Facade**, а не о Laravel-фасадах). Единственная проблема, которая осталась, это создание экземпляров **ImageUploader** с нужными зависимостями для каждого случая. Она может быть решена комбинацией шаблонов **Builder** и **Factory**, либо конфигурацией DI-контейнера. -Признаться, я этот ООП путь привел лишь для того, чтобы показать, что "так тоже можно". -Для текущего примера решение выглядит чересчур громоздким. -Попробуем другой вариант. +Признаться, я этот ООП путь привел лишь для того, чтобы показать, что «так тоже можно». Для текущего примера решение выглядит чересчур громоздким. Попробуем другой вариант. ### Configuration way -Я буду использовать файл конфигурации Laravel чтобы хранить все настройки. +Я буду использовать файл конфигурации Laravel, чтобы хранить все настройки. config/image.php: ```php @@ -1022,7 +1017,7 @@ return [ ]; ``` -Класс **ImageUploader** использующий конфигурацию (интерфейс **Repository**): +Класс **ImageUploader**, использующий конфигурацию (интерфейс **Repository**): ```php final class ImageUploader @@ -1065,12 +1060,11 @@ final class ImageUploader $fileContent = $file->getContents(); $options = $this->config->get('image.' . $type); - if(Arr::get($options, 'check', true)) { - + + if (Arr::get($options, 'check', true)) { $weak = Arr::get($options, 'weak', false); - if(!$this->imageGuard->check($fileContent, $weak)){ - + if (!$this->imageGuard->check($fileContent, $weak)){ if(Arr::get($options, 'ban', true)) { $this->listener->handle($uploadedBy); } @@ -1092,7 +1086,7 @@ final class ImageUploader } ``` -Да, код не выглядит столько чистым как в ООП-варианте, но его конфигурация и реализация весьма простые. +Да, код не выглядит столь чистым как в ООП-варианте, но его конфигурация и реализация весьма просты. Для загрузки картинок этот вариант явно оптимальнее, но в других случаях с более сложной конфигурацией или оркестрацией, ООП-вариант будет предпочтительнее. ## Расширение интерфейсов @@ -1129,7 +1123,7 @@ foreach ($events as $event) ``` Но копипастить это в каждом методе сервисов не очень хочется. -Языки C# и Kotlin имеют фичу "метод-расширение", который натурально "добавляет" метод к любому классу или интерфейсу: +Языки C# и Kotlin имеют фичу «метод-расширение», который натурально «добавляет» метод к любому классу или интерфейсу: ```c# namespace ExtensionMethods @@ -1201,7 +1195,7 @@ final class SomeService extends BaseService } ``` -Использование наследования для того, чтобы унаследовать функционал - не очень хорошая идея. +Использование наследования для того, чтобы унаследовать функционал — не очень хорошая идея. Конструкторы становятся более сложными со всеми этими **parent::__construct** вызовами. Расширение другого интерфейса этим же базовым классом повлечет за собой изменения конструкторов всех сервисов. @@ -1250,7 +1244,7 @@ class AppServiceProvider extends ServiceProvider } ``` -Класс **BaseService** может быть удален и сервис-классы могут просто использовать этот новый интерфейс: +Класс **BaseService** может быть удалён и сервис-классы могут просто использовать этот новый интерфейс: ```php final class SomeService @@ -1297,8 +1291,7 @@ final class LaravelMultiDispatcher public function multiDispatch(array $events) { - foreach($events as $event) - { + foreach($events as $event) { $this->dispatcher->dispatch($event); } } @@ -1319,19 +1312,18 @@ final class LaravelMultiDispatcher ``` Для больших интерфейсов это может быть весьма долгим, рутинным действием. -Некоторые IDE для других языков(как C#) могут генерировать такой код делегации интерфейса автоматически. +Некоторые IDE для других языков (как C#) могут генерировать такой код делегации интерфейса автоматически. Я надеюсь, для PHP тоже скоро реализуют. ## Трейты Трейты в PHP позволяют магически добавлять функционал в класс практически бесплатно. Это весьма мощная магия: они могут залезать в приватные части классов и добавлять новые публичные и даже приватные методы в них. -Я не люблю их. -Это часть тёмной магии PHP, мощной и опасной. +Я не люблю их. Это часть тёмной магии PHP, мощной и опасной. Они могут с успехом использоваться в классах тестов, поскольку там нет хорошей причины организовывать полноценное DI, но лучше избегать их использования в главном коде приложения. -Трейты - это не ООП, а чистые ООП решения всегда будут более естественными. +Трейты — это не ООП, а чистые ООП решения всегда будут более естественными. -### Трейты расширяющие интерфейсы +### Трейты, расширяющие интерфейсы Проблема с множественной обработкой событий может быть решена с помощью трейта: @@ -1340,8 +1332,7 @@ trait MultiDispatch { public function multiDispatch(array $events) { - foreach($events as $event) - { + foreach($events as $event) { $this->dispatcher->dispatch($event); } } @@ -1370,8 +1361,7 @@ final class SomeService ``` Трейт **MultiDispatch** предполагает, что у класса, который будет его использовать есть поле **dispatcher** класса **Dispatcher**. -Лучше не делать таких неявных предположений. -Решение с отдельным интерфейсом **MultiDispatcher** намного более явное и стабильное. +Лучше не делать таких неявных предположений. Решение с отдельным интерфейсом **MultiDispatcher** намного более явное и стабильное. ### Трейты как части класса @@ -1396,8 +1386,7 @@ foo.bar(); foo.bar2(); ``` -Когда тоже самое случается в PHP, трейты используются с той же целью. -Пример из Laravel: +Когда тоже самое случается в PHP, трейты используются с той же целью. Пример из Laravel: ```php class Request extends SymfonyRequest @@ -1409,7 +1398,7 @@ class Request extends SymfonyRequest ``` Большой класс **Request** разделен на несколько составляющих. -Когда класс "хочет" разделиться на несколько - это большой намек на то, что у него слишком много ответственностей. +Когда класс «хочет» разделиться на несколько — это большой намек на то, что у него слишком много ответственностей. Класс **Request** вполне можно было бы скомпоновать из нескольких других классов, таких как **Session**, **RequestInput**, **Cookies** и т.д. ```php @@ -1464,9 +1453,8 @@ class Request ### Трейты как поведение -Eloquent трейты, такие как **SoftDeletes** - примеры поведенческих трейтов. -Они изменяют поведение классов. -Классы Eloquent моделей содержат как минимум две ответственности: хранение состояния сущности и выборку/сохранение/удаление сущностей из базы, поэтому Eloquent трейты тоже могут менять то, как модели взаимодействуют с базой данных, а также добавлять новые поля и методы в них. +Eloquent-трейты, такие как **SoftDeletes** - примеры поведенческих трейтов. Они изменяют поведение классов. +Классы Eloquent моделей содержат как минимум две ответственности: хранение состояния сущности и выборку/сохранение/удаление сущностей из базы, поэтому Eloquent-трейты тоже могут менять то, как модели взаимодействуют с базой данных, а также добавлять новые поля и методы в них. Эти трейты надо как-то конфигурировать и тут раскрывается большой простор для фантазии разработчиков пакетов. Трейт **SoftDeletes**: @@ -1488,9 +1476,7 @@ trait SoftDeletes ``` Он ищет константу **DELETED_AT** в классе и если находит, то использует её значение для имени поля, либо использует стандартное. -Даже для такой простейшей конфигурации была применена магия (функция **defined**). -Другие Eloquent трейты имеют более сложную конфигурацию. -Я нашел одну библиотеку и Eloquent трейт там выглядит так: +Даже для такой простейшей конфигурации была применена магия (функция **defined**). Другие Eloquent трейты имеют более сложную конфигурацию. Я нашел одну библиотеку и Eloquent трейт там выглядит так: ```php trait DetectsChanges @@ -1512,8 +1498,7 @@ trait DetectsChanges } ``` -Простая настройка, а сколько сложностей. -Просто представьте: +Простая настройка, а сколько сложностей. Просто представьте: ```php class SomeModel @@ -1530,17 +1515,15 @@ class SomeModel } ``` -Явная настройка поведения с удобной конфигурацией, которую будет подсказывать ваша среда разработки, без замусоривания исходного класса. -Идеально! +Явная настройка поведения с удобной конфигурацией, которую будет подсказывать ваша среда разработки, без замусоривания исходного класса. Идеально! Поля и отношения в Eloquent виртуальные, поэтому эта реализация поведений тоже возможна. -Без магии, конечно, не обойдется, это все-таки Eloquent, но выглядит намного более объектно-ориентированно и, что намного важнее, явно. +Без магии, конечно, не обойдётся, это все-таки Eloquent, но выглядит намного более объектно-ориентированно и, что намного важнее, явно. Разумеется, эти behaviours существуют только в моем воображении и, вероятно, я не вижу некоторых проблем, но эта идея мне нравится намного больше, чем трейты. ### Бесполезные трейты -Некоторые трейты просто абсолютно бесполезны. -Я нашел один такой в исходниках Laravel: +Некоторые трейты просто абсолютно бесполезны. Я нашел один такой в исходниках Laravel: ```php trait DispatchesJobs @@ -1592,7 +1575,7 @@ class WantsToDispatchJobs } ``` -Эта простота - главная причина того, что разработчики не используют Внедрение зависимостей в PHP. +Эта простота — главная причина того, что разработчики не используют внедрение зависимостей в PHP. ```php class WantsToDispatchJobs @@ -1617,7 +1600,7 @@ class WantsToDispatchJobs Этот класс намного проще прошлых примеров, поскольку имеет явную зависимость от **Dispatcher**. Он явно постулирует, что для работы ему необходим диспатчер. В случае, когда этот класс захотят перенести в другой проект или написать тесты для него, разработчикам не придется полностью сканировать его код и искать эти глобальные вызовы функций или фасадов. -Единственная проблема - громоздкий синтаксис с конструктором и приватное поле. +Единственная проблема — громоздкий синтаксис с конструктором и приватное поле. Синтаксис в языке Kotlin намного более элегантный: ```kotlin @@ -1628,18 +1611,18 @@ class WantsToDispatchJobs(private val dispatcher: Dispatcher) } ``` -Синтаксис PHP является некоторым барьером для использования DI, я надеюсь скоро это будет нивелировано либо изменениями в синтаксисе, либо инструментами в средах разработки. +Синтаксис PHP является некоторым барьером для использования DI и я надеюсь, что скоро это будет нивелировано либо изменениями в синтаксисе, либо инструментами в средах разработки. После нескольких лет использования и неиспользования трейтов я могу сказать, что разработчики используют трейты по двум причинам: -* борясь с последствиями архитектурных проблем -* создавая архитектурные проблемы (иногда не осознавая этого) +* борясь с последствиями архитектурных проблем; +* создавая архитектурные проблемы (иногда не осознавая этого). Надо лечить болезнь, а не симптомы, поэтому лучше найти причины, заставляющие нас использовать трейты и постараться их исправить. ## Статические методы -Я писал, что используя статические методы и классы мы создаем жесткую связь, но иногда это нормально. +Я писал, что используя статические методы и классы мы создаем жёсткую связь, но иногда это нормально. Пример из прошлой главы: ```php @@ -1662,24 +1645,21 @@ final class CacheKeys $key = CacheKeys::getUserByIdKey($id); ``` -Ключи кеширования необходимы в двух местах: в классах декораторах для выборки сущностей и в классах-слушателей событий, который отлавливают события изменения сущностей и чистят кеш. +Ключи кэширования необходимы в двух местах: в классах декораторах для выборки сущностей и в классах-слушателей событий, которые отлавливают события изменения сущностей и чистят кэш. Я мог бы использовать класс **CacheKeys** через DI, но в этом мало смысла. -Все эти классы декораторов и слушателей формируют некую структуру, которую можно назвать "Модуль кеширования" для этого приложения. -Класс **CacheKeys** будет приватной частью этого модуля. -Никакой другой код приложения не должен об этом классе знать. +Все эти классы декораторов и слушателей формируют некую структуру, которую можно назвать «модуль кэширования» для этого приложения. +Класс **CacheKeys** будет приватной частью этого модуля. Никакой другой код приложения не должен об этом классе знать. ![](images/cache_module.png) -Использование статических методов для таких внутренних зависимостей, которые не работают с внешним миром (файлами, БД или API) - нормальная практика. +Использование статических методов для таких внутренних зависимостей, которые не работают с внешним миром (файлами, БД или API) — нормальная практика. ## Пара слов в конце главы -Самое большое преимущество использования Внедрения Зависимостей это явный и четкий контракт класса. -Публичные методы говорят о том, какую работу способен этот класс исполнять. -Параметры конструктора говорят о том, что нужно для этой работы этому классу. -В больших, долго длящихся, проектах это очень важно. Классы могут быть легко протестированы и использованы в любых условиях. Необходимо лишь предоставить им нужные зависимости. -Вся эта магия, как методы **__call**, фасады Laravel и трейты разрушают эту гармонию. +Самое большое преимущество использования внедрения зависимостей это явный и чёткий контракт класса. +Публичные методы говорят о том, какую работу способен этот класс исполнять. Параметры конструктора говорят о том, что нужно для этой работы этому классу. +В больших, долго длящихся проектах это очень важно. Классы могут быть легко протестированы и использованы в любых условиях. Необходимо лишь предоставить им нужные зависимости. Вся эта магия, как методы **__call**, фасады Laravel и трейты разрушают эту гармонию. -С другой стороны, мне сложно представить, например, HTTP контроллеры вне приложения Laravel и, я надеюсь, никто не пишет юнит-тесты для них. +С другой стороны, мне сложно представить, например, HTTP-контроллеры вне приложения Laravel и, я надеюсь, никто не пишет юнит-тесты для них. Поэтому, это вполне подходящее место для использования функций-хелперов (**redirect()**, **view()**) и Laravel фасадов (**\Response**, **\URL**).