diff --git a/manuscript/1-bad-habits.md b/manuscript/1-bad-habits.md index 14b305a..6c7c6ce 100644 --- a/manuscript/1-bad-habits.md +++ b/manuscript/1-bad-habits.md @@ -1,5 +1,7 @@ # Плохие привычки +A> "Сегодня курение спасет жизни!" + ## Проблемы роста В этой главе я попытаюсь показать, как обычно проекты растут и решают возникающие проблемы. diff --git a/manuscript/2-di.md b/manuscript/2-di.md index 3cf4684..181dcce 100644 --- a/manuscript/2-di.md +++ b/manuscript/2-di.md @@ -1,5 +1,7 @@ # Внедрение зависимостей +A> "Дайте мне точку опоры и я переверну землю" + ## Принцип Единственной Ответственности Вы, возможно, слышали о Принципе Единственной Ответственности (Single Responsibility Principle, SRP). diff --git a/manuscript/3-painless-refactoring.md b/manuscript/3-painless-refactoring.md index f8e3562..703e378 100644 --- a/manuscript/3-painless-refactoring.md +++ b/manuscript/3-painless-refactoring.md @@ -1,6 +1,6 @@ # Безболезненный рефакторинг -A> Надежно зафиксированный больной не нуждается в анестезии. Народная мудрость. +A> "Надежно зафиксированный больной не нуждается в анестезии." ## "Статическая" типизация diff --git a/manuscript/4-application-layer.md b/manuscript/4-application-layer.md index ba28e9f..2d1d4ff 100644 --- a/manuscript/4-application-layer.md +++ b/manuscript/4-application-layer.md @@ -1,5 +1,7 @@ # Слой Приложения +A> "Я Винстон Вульф. Я решаю проблемы." + Продолжаем наш пример. Приложение растёт, и в форму регистрации добавились новые поля: дата рождения и опция согласия получения email-рассылки. diff --git a/manuscript/5-error-handling.md b/manuscript/5-error-handling.md index 6738c3b..14bae9a 100644 --- a/manuscript/5-error-handling.md +++ b/manuscript/5-error-handling.md @@ -1,6 +1,6 @@ # Обработка ошибок -A> "Каждый заслуживает второй второй шанс, Пэм!" Майкл Скотт. +A> "Каждый заслуживает второй второй шанс, Пэм!" Язык С, который дал основу синтаксиса для многих современных языков, имеет простую конвенцию для ошибок. Если функция должна вернуть какие-то данные, но не может вернуть из-за ошибки, она возвращает null. diff --git a/manuscript/7-events.md b/manuscript/7-events.md index 78bf026..d50ae6c 100644 --- a/manuscript/7-events.md +++ b/manuscript/7-events.md @@ -1,7 +1,5 @@ # События -A> Правило прокрастинатора: если что-то можно отложить, это надо отложить. - Действие Слоя Приложения всегда содержит главную часть, где выполняется основная логика действия, а также некоторые дополнительные действия. Регистрация пользователя состоит из, собственно, создания сущности пользователя, а также посылки соответствующего письма. Обновление текста статьи содержит обновление значения **$post->text** с сохранением сущности, а также, например вызов **Cache::forget** для инвалидации кеша. @@ -12,25 +10,25 @@ A> Правило прокрастинатора: если что-то можн final class SurveyService { public function __construct( - private BadWordsFilter $badWordsFilter + private ProfanityFilter $profanityFilter /*, ... другие зависимости */ ) {} public function create(SurveyCreateDto $request) { $survey = new Survey(); - $survey->question = $this->badWordsFilter->filter( + $survey->question = $this->profanityFilter->filter( $request->getQuestion()); //... $survey->save(); - foreach($request->getAnswers() as $answerText) + foreach($request->getOptions() as $optionText) { - $answer = new SurveyAnswer(); - $answer->survey_id = $survey->id; - $answer->text = - $this->badWordsFilter->filter($answerText); - $answer->save(); + $option = new SurveyOption(); + $option->survey_id = $survey->id; + $option->text = + $this->profanityFilter->filter($optionText); + $option->save(); } // Вызов генерации sitemap @@ -65,17 +63,17 @@ final class SurveyService { $this->connection->transaction(function() use ($request) { $survey = new Survey(); - $survey->question = $this->badWordsFilter->filter( + $survey->question = $this->profanityFilter->filter( $request->getQuestion()); //... $survey->save(); - foreach($request->getAnswers() as $answerText) { - $answer = new SurveyAnswer(); - $answer->survey_id = $survey->id; - $answer->text = - $this->badWordsFilter->filter($answerText); - $answer->save(); + foreach($request->getOptions() as $optionText) { + $option = new SurveyOption(); + $option->survey_id = $survey->id; + $option->text = + $this->profanityFilter->filter($optionText); + $option->save(); } // Вызов генерации sitemap @@ -107,12 +105,12 @@ public function create(SurveyCreateDto $request) //... $survey->save(); - foreach($filteredRequest->getAnswers() - as $answerText) { - $answer = new SurveyAnswer(); - $answer->survey_id = $survey->id; - $answer->text = $answerText; - $answer->save(); + foreach($filteredRequest->getOptions() + as $optionText) { + $option = new SurveyOption(); + $option->survey_id = $survey->id; + $option->text = $optionText; + $option->save(); } }); @@ -303,11 +301,11 @@ public function create(SurveyCreateDto $request) //... $survey->save(); - foreach($filteredRequest->getAnswers() as $answerText){ - $answer = new SurveyAnswer(); - $answer->survey_id = $survey->id; - $answer->text = $answerText; - $answer->save(); + foreach($filteredRequest->getOptions() as $optionText){ + $option = new SurveyOption(); + $option->survey_id = $survey->id; + $option->text = $optionText; + $option->save(); } }); //... @@ -357,17 +355,17 @@ final class SendSurveyCreatedEmailListener implements ShouldQueue public function handle(SurveyCreated $event) { // ... - foreach($event->survey->answers as $answer) + foreach($event->survey->options as $option) {...} } } ``` Это простой пример слушателя, который использует значения **HasMany**-отношения. -Этот код работает. Когда выполняется код **$event->survey->answers** Eloquent делает запрос в базу данных и получает все варианты ответа. +Этот код работает. Когда выполняется код **$event->survey->options** Eloquent делает запрос в базу данных и получает все варианты ответа. Другой пример: ```php -final class SurveyAnswerAdded +final class SurveyOptionAdded { public function __construct( public readonly Survey $survey @@ -376,26 +374,26 @@ final class SurveyAnswerAdded final class SurveyService { -public function addAnswer(SurveyAddAnswerDto $request) +public function addOption(SurveyAddOptionDto $request) { $survey = Survey::findOrFail($request->getSurveyId()); - if($survey->answers->count() >= Survey::MAX_POSSIBLE_ANSWERS) { - throw new BusinessException('Max answers amount exceeded'); + if($survey->options->count() >= Survey::MAX_POSSIBLE_OPTIONS) { + throw new BusinessException('Max options amount exceeded'); } - $survey->answers()->create(...); + $survey->options()->create(...); - $this->dispatcher->dispatch(new SurveyAnswerAdded($survey)); + $this->dispatcher->dispatch(new SurveyOptionAdded($survey)); } } final class SomeListener implements ShouldQueue { - public function handle(SurveyAnswerAdded $event) + public function handle(SurveyOptionAdded $event) { // ... - foreach($event->survey->answers as $answer) + foreach($event->survey->options as $option) {...} } } @@ -403,13 +401,13 @@ final class SomeListener implements ShouldQueue А вот тут уже не все хорошо. Когда сервисный класс проверяет количество вариантов ответа, он получает свежую коллекцию текущих вариантов ответа данного опроса. -Потом он добавляет новый вариант ответа, вызвав **$survey->answers()->create(...)**; -Дальше, слушатель, выполняя **$event->survey->answers** получает старую версию вариантов ответа, без новосозданной. -Это поведение Eloquent, который имеет два механизма работы с отношениями. Метод **answers()** и псевдо-поле **answers**, которое вроде бы и соответствует этому методу, но хранит свою версию данных. +Потом он добавляет новый вариант ответа, вызвав **$survey->options()->create(...)**; +Дальше, слушатель, выполняя **$event->survey->options** получает старую версию вариантов ответа, без новосозданной. +Это поведение Eloquent, который имеет два механизма работы с отношениями. Метод **options()** и псевдо-поле **options**, которое вроде бы и соответствует этому методу, но хранит свою версию данных. Поэтому, передавая сущность в события, разработчик должен озаботиться консистентностью значений в отношениях, например вызвав: ```php -$survey->load('answers'); +$survey->load('options'); ``` до передачи объекта в событие. diff --git a/manuscript/8-unit-test.md b/manuscript/8-unit-test.md index c7c974b..9d74fd3 100644 --- a/manuscript/8-unit-test.md +++ b/manuscript/8-unit-test.md @@ -1,7 +1,5 @@ # Unit-тестирование -A> 100% покрытие тестами должно быть следствием, а не целью. - ## Первые шаги Вы, вероятно, уже слышали про unit-тестирование. @@ -682,13 +680,13 @@ Laravel не только предлагает их использовать, н ```php final class Survey extends Model { - public function answers() + public function options() { - return $this->hasMany(SurveyAnswer::class); + return $this->hasMany(SurveyOption::class); } } -final class SurveyAnswer extends Model +final class SurveyOption extends Model { } @@ -704,7 +702,7 @@ final class SurveyCreateDto public function __construct( public readonly string $title, /** @var string[] */ - public readonly array $answers, + public readonly array $options, ) {} } @@ -712,9 +710,9 @@ final class SurveyService { public function create(SurveyCreateDto $dto) { - if(count($dto->answers) < 2) { + if(count($dto->options) < 2) { throw new BusinessException( - "Please provide at least 2 answers"); + "Please provide at least 2 options"); } $survey = new Survey(); @@ -723,9 +721,9 @@ final class SurveyService $survey->title = $dto->title; $survey->save(); - foreach ($dto->answers as $answer) { - $survey->answers()->create([ - 'text' => $answer, + foreach ($dto->options as $option) { + $survey->options()->create([ + 'text' => $option, ]); } }); @@ -743,7 +741,7 @@ class SurveyServiceTest extends TestCase $postService = new SurveyService(); $postService->create(new SurveyCreateDto( 'test title', - ['answer1', 'answer2'])); + ['option1', 'option2'])); \Event::assertDispatched(SurveyCreated::class); } @@ -811,24 +809,16 @@ class DatabaseManager ```php class SurveyService { - /** @var \Illuminate\Database\ConnectionInterface */ - private $connection; - - /** @var \Illuminate\Contracts\Events\Dispatcher */ - private $dispatcher; - public function __construct( - ConnectionInterface $connection, Dispatcher $dispatcher) - { - $this->connection = $connection; - $this->dispatcher = $dispatcher; - } + private ConnectionInterface $connection, + private Dispatcher $dispatcher + ) {} public function create(SurveyCreateDto $dto) { - if(count($dto->answers) < 2) { + if(count($dto->options) < 2) { throw new BusinessException( - "Please provide at least 2 answers"); + "Please provide at least 2 options"); } $survey = new Survey(); @@ -837,9 +827,9 @@ class SurveyService $survey->title = $dto->title; $survey->save(); - foreach ($dto->answers as $answer) { - $survey->answers()->create([ - 'text' => $answer, + foreach ($dto->options as $option) { + $survey->options()->create([ + 'text' => $option, ]); } }); @@ -868,7 +858,7 @@ class SurveyServiceTest extends TestCase $postService->create(new SurveyCreateDto( 'test title', - ['answer1', 'answer2'])); + ['option1', 'option2'])); $eventFake->assertDispatched(SurveyCreated::class); } @@ -918,7 +908,7 @@ interface SurveyRepository public function save(Survey $survey); - public function saveAnswer(SurveyAnswer $answer); + public function saveOption(SurveyOption $option); } class EloquentSurveyRepository implements SurveyRepository @@ -930,38 +920,25 @@ class EloquentSurveyRepository implements SurveyRepository $survey->save(); } - public function saveAnswer(SurveyAnswer $answer) + public function saveOption(SurveyOption $option) { - $answer->save(); + $option->save(); } } class SurveyService { - /** @var \Illuminate\Database\ConnectionInterface */ - private $connection; - - /** @var SurveyRepository */ - private $repository; - - /** @var \Illuminate\Contracts\Events\Dispatcher */ - private $dispatcher; - public function __construct( - ConnectionInterface $connection, - SurveyRepository $repository, - Dispatcher $dispatcher) - { - $this->connection = $connection; - $this->repository = $repository; - $this->dispatcher = $dispatcher; - } + private ConnectionInterface $connection, + private SurveyRepository $repository, + private Dispatcher $dispatcher + ) {} public function create(SurveyCreateDto $dto) { - if(count($dto->answers) < 2) { + if(count($dto->options) < 2) { throw new BusinessException( - "Please provide at least 2 answers"); + "Please provide at least 2 options"); } $survey = new Survey(); @@ -970,12 +947,12 @@ class SurveyService $survey->title = $dto->title; $this->repository->save($survey); - foreach ($dto->answers as $answerText) { - $answer = new SurveyAnswer(); - $answer->survey_id = $survey->id; - $answer->text = $answerText; + foreach ($dto->options as $optionText) { + $option = new SurveyOption(); + $option->survey_id = $survey->id; + $option->text = $optionText; - $this->repository->saveAnswer($answer); + $this->repository->saveOption($option); } }); @@ -998,14 +975,14 @@ class SurveyServiceTest extends \PHPUnit\Framework\TestCase })); $repositoryMock->expects($this->at(2)) - ->method('saveAnswer'); + ->method('saveOption'); $postService = new SurveyService( new FakeConnection(), $repositoryMock, $eventFake); $postService->create(new SurveyCreateDto( 'test title', - ['answer1', 'answer2'])); + ['option1', 'option2'])); $eventFake->assertDispatched(SurveyCreated::class); } diff --git a/manuscript/9-domain-layer.md b/manuscript/9-domain-layer.md index e148f87..95190d2 100644 --- a/manuscript/9-domain-layer.md +++ b/manuscript/9-domain-layer.md @@ -1,7 +1,5 @@ # Доменный слой -A> private $name, getName() и setName($name) это НЕ инкапсуляция! - ## Когда и зачем? Бизнес-логика, на английском Domain logic или Логика предметной области, это та логика, которую представляют себе пользователи или заказчики, если полностью выкинуть из головы интерфейс пользователя. Например, для игры это будет полный свод её правил, а для финансового приложения - все сущности, которые там есть и все правила расчетов. Для блога всю бизнес-логику можно грубо описать так: есть статьи, у них есть заголовок и текст, администратор может их создать и опубликовать, а другие пользователи могут видеть все опубликованные статьи. @@ -83,9 +81,9 @@ Eloquent является реализацией шаблона Active Record. ### Высокая связность бизнес логики -Вернёмся к примеру с сущностью опроса. Опрос - весьма простая сущность, которая содержит текст вопроса и возможные ответы. Очевидное условие: у каждого опроса должно быть как минимум два варианта ответа. В примере, который был раньше в книге, в действии **создатьОпрос** была такая проверка перед созданием объекта сущности. Это же условие делает сущность **SurveyAnswer** зависимой. Приложение не может просто взять эту сущность и удалить её. В действии **удалитьВариантОтвета** сначала должно быть проверено, что в объекте **Survey** после этого останется достаточно вариантов ответа. Таким образом, знание о том, что в опросе должно быть как минимум два варианта ответа, теперь содержится в обоих этих действиях: **создатьОпрос** и **удалитьВариантОтвета**. Связность данного кода слабая. +Вернёмся к примеру с сущностью опроса. Опрос - весьма простая сущность, которая содержит текст вопроса и возможные ответы. Очевидное условие: у каждого опроса должно быть как минимум два варианта ответа. В примере, который был раньше в книге, в действии **создатьОпрос** была такая проверка перед созданием объекта сущности. Это же условие делает сущность **SurveyOption** зависимой. Приложение не может просто взять эту сущность и удалить её. В действии **удалитьВариантОтвета** сначала должно быть проверено, что в объекте **Survey** после этого останется достаточно вариантов ответа. Таким образом, знание о том, что в опросе должно быть как минимум два варианта ответа, теперь содержится в обоих этих действиях: **создатьОпрос** и **удалитьВариантОтвета**. Связность данного кода слабая. -Это происходит из-за того, что сущности **Survey** и **SurveyAnswer** не являются независимыми. Они представляют собой один **модуль** - опрос с вариантами ответа. Знание о минимальном количестве вариантов ответа должно быть сосредоточено в одном месте - сущности **Survey**. +Это происходит из-за того, что сущности **Survey** и **SurveyOption** не являются независимыми. Они представляют собой один **модуль** - опрос с вариантами ответа. Знание о минимальном количестве вариантов ответа должно быть сосредоточено в одном месте - сущности **Survey**. Я понимаю, что такой простой пример не может доказать важность работы таких сущностей как одно целое. Представим что-нибудь более сложное - реализацию игры Монополия! Всё в этой игре - это один большой модуль. Игроки, их имущество, их деньги, их положение на доске, положение других объектов на доске. Всё это представляет собой текущее состояние игры. Игрок делает ход и всё, что произойдёт дальше, зависит от текущего состояния. Если он наступает на чужую собственность - он должен заплатить. Если у него достаточно денег - он платит. Если нет - должен получить деньги как-либо, либо сдаться. Если собственность ничья, её можно купить. Если у него недостаточно денег - должен начаться аукцион среди других игроков. @@ -106,7 +104,7 @@ Eloquent является реализацией шаблона Active Record. Кстати, идея иммутабельных объектов, т.е. объектов, которые создаются раз и не меняют никаких своих значений, делает задачу поддержания инварианта простой. Пример такого объекта: объект-значение **Email**. Его инвариант - содержать всегда только правильное значение строки email-адреса. Проверка инварианта делается в конструкторе. А поскольку он дальше не меняет своего состояния, то в других местах эта проверка и не нужна. А вот если бы он имел метод **setEmail**, то проверку инварианта, т.е. корректности email, пришлось бы вызывать и в этом методе. -В сущностях Eloquent крайне трудно обеспечивать инварианты. Объекты **SurveyAnswer** - формально независимы, хотя и должны быть под контролем объекта **Survey**. Любая часть приложения может вызвать **remove()** метод сущности **SurveyAnswer** и она будет просто удалена. В итоге все инварианты Eloquent сущностей держатся буквально на честном слове: на соглашениях внутри проекта или отлаженном процессе code review. Серьезным проектам необходимо что-то более весомое, чем честное слово. Системы, в которых сам код не позволяет делать больших глупостей, намного более стабильны, чем системы полагающие, что ими будут заниматься только очень умные и высоко-квалифицированные программисты. +В сущностях Eloquent крайне трудно обеспечивать инварианты. Объекты **SurveyOption** - формально независимы, хотя и должны быть под контролем объекта **Survey**. Любая часть приложения может вызвать **remove()** метод сущности **SurveyOption** и она будет просто удалена. В итоге все инварианты Eloquent сущностей держатся буквально на честном слове: на соглашениях внутри проекта или отлаженном процессе code review. Серьезным проектам необходимо что-то более весомое, чем честное слово. Системы, в которых сам код не позволяет делать больших глупостей, намного более стабильны, чем системы полагающие, что ими будут заниматься только очень умные и высоко-квалифицированные программисты. ## Реализация Доменного слоя