Александр Карпов
388 subscribers
26 photos
1 video
39 links
Блог PHP разработчика для шаринга личного опыта с друзьями, коллегами и всеми желающими.

Не стесняйтесь задавать вопросы в комментариях к постам, или в личку: @slayervc (UTC +8).
Download Telegram
Как понять слои. Слой Application

Application - это слой оркестрации. В него мы помещаем код, который:

⚙️ Непосредственно реализует выполнение конкретных действий.
⚙️ Координирует различные части доменного слоя.

Слой Application проще всего организовывать в виде классов-команд. Сами эти классы могут называться по-разному: Command, UseCase, Handler и т. д.

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

1️⃣ Сущности User, Community и Invitation
2️⃣ Репозитории для сохранения
3️⃣ Транзакция для атомарности
4️⃣ Контроллер для вызова с фронта

Первый пункт - это доменный слой, остальные три - инфраструктурный. А слой Appication - это то, что свяжет воедино все эти компоненты. Для реализации поставленной задачи нам понадобятся классы двух команд:

⚙️ Создания приглашения
⚙️ Принятия приглашения пользователем

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

namespace App\Application\Command\Community;

class AcceptInvitation
{
public function __construct(
private readonly InvitationRepositoryInterface $invitationRepository,
private readonly CommunityRepositoryInterface $communityRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly TransactionalSessionInterface $transactionalSession
) {
}

public function execute(AcceptInvitationParams $params): void
{
$invitation = $this->invitationRepository->getById($params->invitationId);
$community = $this->communityRepository->getById($invitation->getCommunityId());
$user = $this->userRepository->getById($params->userId);
$invitation->accept($user, $community);

$this->transactionalSession->transactional(function () use ($community, $invitation, $user) {
$this->communityRepository->save($community);
$this->userRepository->save($user);
$this->invitationRepository->delete($invitation);
});
}
}


Как видите, команды Application довольно просты и почти всегда реализуют один и тот же алгоритм:

1️⃣ Достать из репозиториев все необходимые сущности.

2️⃣ Вызвать методы сущностей, содержащие бизнес-логику.

3️⃣ Сохранить в репозиториях изменившиеся сущности.

В примере выше метод Invitation::accept() содержит в себе всю бизнес-логику, по присоединению пользователя к Community. TransactionalSessionInterface содержит единственный метод transactional(), который обязан обеспечить атомарность сохранения всех сущностей. Как именно это будет достигаться - проблема инфраструктурного слоя, здесь мы об этом не думаем.

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

☘️ из контроллера,
☘️ из асинхронного обработчика AMQP сообщения ,
☘️ из консольной команды, и т. д.

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

Резюмируя:

☘️ Держите слой Application самым тонким в вашем приложении.

☘️ Классы-команды слоя Application должны зависеть от абстрактных интерфейсов, а не от конкретных инфраструктурных компонентов.

☘️ Старайтесь любой код, реализующий проверки правил, спускать в доменный слой. Слой Application должен быть максимально тупым.

Для дополнительного примера прикрепил к посту скрин содержимого папки Application одного из моих проектов. Если у вас появляются вопросы по слоям, пишите их в комментариях или в личку.🙂

#слои
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9👍1
Доменный слой и логирование

Отвечаю на вопрос подписчика о логировании, исключениях и доменном слое.

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

class Course
{
public function __construct(
string $id,
string $name,
MaterialCollection $materials,
TestCollection $tests
) {
foreach ($tests->getElements() as $test) {
if (!$materials->hasMaterial($test->getMaterialId())) {
throw new \DomainException('Attempting to add Test to Course without adding appropriate material');
}
}

if (!$materials->allElementsActive()) {
throw new \DomainException('Course can not contain non-active materials');
}

//...
}
}


В примере выше контролируется соблюдение двух правил:

1️⃣ Если учебный курс содержит тесты, то эти тесты должны быть по материалам, входящим в курс.

2️⃣ Все материалы, входящие в учебный курс, должны быть опубликованы (status === 'active').

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

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

Таким образом, возникновение исключений в доменном слое с высокой долей вероятности должно привести к изменениям в вашем коде.


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

Фреймворки, как правило, имеют exception- и errorHandler'ы, которые автоматически логируют (или хотя бы перехватывают) все исключения. От разработчика, как правило, требуется только настроить/кастомизировать их.

Разумеется, все, что связано с логированием - это инфраструктурный слой. Все кастомные форматтеры логов и тому подобные классы вы помещаете в Infrastructure.

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

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

#слои
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥42👍2👏1
Простое и полезное правило из DDD

Когда мне нужно ввести человека, не знакомого с DDD, в проект, в качестве самого первого и важного шага я стараюсь добиться соблюдения следующего правила:

Доступ к вложенным в агрегат сущностям возможен только через корень агрегата.


Давайте разберемся, что это означает. Доменный агрегат в DDD - это сущность, включающая в себя другие сущности. Зачем и почему, опускаю, иначе не влезу в размер поста.🙂 Вот классический пример доменного агрегата:

class User
{
private string $id;
private \DateTimeImmutable $createdAt;
private Profile $profile;
}

class Profile
{
private string $userId;
private ProfileName $profileName;
}

class ProfileName
{
private ?string $firstName;
private ?string $lastName;
private ?string $middleName;
}


В данном примере сущность User агрегирует в себя сущность Profile, которая в свою очередь агрегирует в себя объект-значение ProfileName. Так вот, User является тем, что называется aggregate root и, согласно DDD, мы не можем писать вот такой код:

$user->getProfile()->setName('Vasya');


Нельзя раздергивать агрегат, то есть вытаскивать наружу его составные части. Инстанс класса Profile не может существовать отдельно от инстанса класса User, то есть:

Нельзя достать из юзера его профиль, что-то поделать с ним, а потом запихать обратно.

Нельзя доставать из базы отдельно профиль пользователя. Если достаем, то достаем весь агрегат User.

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

У вас может возникнуть вопрос: что же теперь, из-за соблюдения DDD будем делать лишние запросы в базу? Не будем, если пользоваться ORM, которая сама отслеживает, какие объекты были изменены, и шлет запросы только для тех сущностей, у которых обновились данные. То же и с чтением: если сущность уже извлечена из базы, ORM должна отдавать ее из кэша.

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

class User
{
public function setProfileName(string $first, string $last, ?string $middle): void
{
$this->profile->setName($first, $last, $middle);
}
}

class Profile
{
public function setName(string $first, string $last, ?string $middle): void
{
$this->profileName->setName($first, $last, $middle);
}
}

class ProfileName
{
public function setName(string $first, string $last, ?string $middle): void
{
$this->firstName = $first;
$this->lastName = $last;
$this->middleName = $middle;
}
}


Единый сеттер для $first, $last и $middle продиктован бизнес-правилом, не позволяющим менять составные части имени по отдельности. Этот подход гораздо лучше такого, когда при объявлении сущности автоматом делаются сеттеры и геттеры на все ее поля. Делали бы тогда их сразу public, да и все. Вот преимущества соблюдения этого простого правила:

🍀 Поддерживается высокий cohesion (сделаю, пожалуй, отдельный пост про него).

🍀 Уменьшается количество интерфейсов, торчащих наружу. Если ваш агрегат включает в себя десяток классов (а это обычное дело), то вам будет гораздо проще, если у вас в клиентском коде используется только один интерфейс агрегат-рута, а не интерфейсы всех 10 классов.

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

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

🍀 Клиентский код, использующий доменные агрегаты, становится тонким, чистым и понятным: ведь вся логика и данные скрыты за интерфейсом агрегата.

#DDD #Агрегат
Please open Telegram to view this post
VIEW IN TELEGRAM
👍143🙏3
Что такое инвариант

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

1️⃣ Учебный курс должен содержать хотя бы один учебный материал.

2️⃣ В учебный курс не могут входить материалы, имеющие статус "черновик".

3️⃣ Если в учебный курс входят тесты, то они должны быть по тем материалам, которые входят в курс.

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

$course = new Course();
$materials = $this->materialRepository->find();
$course->setMaterials($materials);
$tests = $this->testRepositpry->find();
$course->setTests($tests);
$this->courseRepository->save($course);


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

Сравните пример выше вот с таким кодом:

class Course
{
public function __construct(
//...
MaterialCollection $materials,
TestCollection $tests
) {
if ($materials->empty()) {
throw new \DomainException('Course must contain at least one material');
}

if (!$materials->allElementsActive()) {
throw new \DomainException('Course can not contain non-active materials');
}

foreach ($tests->getElements() as $test) {
if (!$materials->hasMaterial($test->getMaterialId())) {
throw new \DomainException('Attempting to add Test to Course without adding appropriate material');
}
}

//...
}
}


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

Соблюдение инвариантов гарантирует консистентность данных на протяжении всего времени выполнения.


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

То есть, у вас никогда не появится "сломанный учебный курс", и вам не придется беспокоиться о том, что в какой-то части приложения не соблюдается бизнес-логика: вы вписали ее так глубоко, что обойти ее теперь невозможно.

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

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

#DDD #Агрегат
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9👍32
Реализация разновидностей объектов: поле type против наследования

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

☘️ Добавлять новые виды организаций.

☘️ Проводить грань между общими и индивидуальными чертами видов организаций.

☘️ При необходимости превращать общие черты в индивидуальные и наоборот.

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

Давайте рассмотрим первый, самый трешовый и прямолинейный вариант решения этой задачи в одном классе.

enum OrganizationType
{
case HOLDING;
case FRANCHISE;
case OUTLET;
}

class Organization
{
private OrganizationType $type

public function addEmployee(User $employee): void
{
if (OrganizationType::HOLDING === $this->type) {
//...
} elseif (OrganizationType::FRANCHISE === $this->type) {
//...
}

//...
}

public function bindTag(Tag $tag): void
{
if (OrganizationType::HOLDING === $this->type) {
throw new \DomainException('Only holdings may have tags')
}

//...
}
}


Я думаю, минусы такого решения объяснять не нужно: выполнение любого из перечисленных вначале поста пунктов, включая простое добавление новых видов организаций, при таком подходе превращается в боль, а сам код становится очень тяжело читать.

Теперь давайте посмотрим на второй способ решения задачи: через наследование.

class Organization
{
abstract public function addEmployee(User $employee): void;
}

class Holding extends Organization
{
public function addEmployee(User $employee): void
{
//...
}

public function bindTag(Tag $tag): void
{
//...
}
}

class Franchise extends Organization {}

class Outlet extends Organization {}


Что мы получили?

☘️ Легко добавлять новые виды организаций.

☘️ Код стал тоньше, легкочитаемее.

☘️ Общие черты можно разместить в родительском классе, а частные (казалось бы) - в дочерних.

Но так ли все радужно?

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

Нам нужно сделать что-то вроде:

class Outlet extends Organization {
public function addEmployee(User $employee, string $cashBoxId): void
}


Но мы не можем так сделать, поскольку сигнатура метода addEmployee() диктуется родительским классом Organization.

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


А теперь представьте, что где-то в клиентском коде есть такой метод:

public function doSomething(Organization $organization)


Допустим, в этом методе вам нужно по какой-то причине вызвать метод Holding::bindTag(), который представлен только в этом классе, и не представлен в родительском классе Organization. Вам придется делать одно из двух:

👎 объявлять метод bindTag() в классе Organization и делать реализации-зашлушки во всех его наследниках, или

👎 писать в клиентском коде instanceof.

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

В следующем посте рассмотрим преимущества композиции над наследованием для решение этой же задачи.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍153🔥2
Реализация разновидностей объектов: используем композицию

В предыдущем посте ⬆️ мы рассмотрели два инструмента реализации разновидностей объектов: через поле type и с помощью наследования. Оба этих способа имеют свои ограничения. Напомню пример: нужно реализовать три разновидности организаций: Holding, Franchise и Outlet. Для всех видов организаций существует операция добавления в нее нового сотрудника:

class Organization
{
public function addEmployee(User $employee): void
}


Предположим, при добавлении сотрудника к торговой точке Outlet нам нужно указать номер кассы, за которой закреплен этот сотрудник:

class Outlet extends Organization {
//не можем так сделать, т. к. сигнатура метода уже определена в родительском классе
public function addEmployee(User $employee, string $cashBoxId): void
}


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

Очевидно, что вот эта часть сигнатуры метода общая для всех видов организаций: addEmployee(User $employee), а вот эта часть - спецэффичная для одного конкретного вида: (string $cashBoxId).

Как обойти это ограничение?

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


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

abstract class EmploymentStrategy
{
abstract public function addEmployee(User $user);
}

class DefaultEmploymentStrategy extends EmploymentStrategy
{
public function addEmployee(User $user)
{
//бизнес-логика добавления сотрудника в организацию
}
}

class CashBoxBindingEmploymentStrategy extends EmploymentStrategy
{
public function __construct(
private readonly string $cashBoxId
) {}

public function addEmployee(User $user)
{
//Привязываем сотрудника к кассе, используя $this->cashBoxId
}
}

class Organization {
private EmploymentStrategy $employmentStrategy

public function addEmployee(User $user) {
//делегируем выполнение метода классу-стратегии
$this->employmentStrategy->addEmployee($user)
}
}


Преимущества такого подхода очевидны:

☘️ Может применяться как в сочетании с наследованием, так и в едином классе с полем type.

☘️ Создается точка расширения для бесконечного количества вариантов бизнес-логики.

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

☘️ Можно скрещивать разные виды бизнес-объектов сочетая разные поведенческие особенности.

☘️ Интерфейсы бизнес-объектов остаются простыми, не зарастая лишними аргументами. Клиентский код упрощается.

Словом, вот вам принцип "предпочитайте композицию наследованию" в действии. 😄

Не стесняйтесь задавать вопросы и предлагать свои темы для постов в комментариях. 🙂
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍5🤗21
Механизм обязательств пользователя

Простая, вроде бы, вещь, но, почему-то ни в одном виденном мною проекте этот механизм не был реализован. Речь идет о ситуации, когда от пользователя требуется совершить какое-то действие до того, как он получит доступ к интерфейсу. Чаще всего это:

🔒 Заполнение личного профиля или профиля компании.

🔒 Внесение оплаты за пользование системой.

🔒 Принятие пользовательского соглашения.

Как привило, с точки зрения бизнес-задачи и пользовательского опыта требуется такая реализация этого механизма, которая:

☘️ Не позволит обойти выполнение действия, например, перейдя по прямому URL в какой-нибудь подраздел нашей системы.

☘️ Будет предсказуемо редиректить пользователя на соответствующую страницу, на которой он сможет выполнить требующееся от него действие. Причем, для каждого действия мы хотели бы видеть отдельную страницу: свою - для оплаты, свою - для профиля и т. д.

Основные ошибки в реализации этой задачи, которые я видел от проекта к проекту, выглядят так:

☠️ Вынесение логики с проверкой выполнения действия на фронт.

☠️ Привязка проверки выполнения действия к какому-либо другому действию. Например, при загрузке профиля компании проверяем оплату. Предполагается, что профиль компании загружается если и не в 100% обращений к бэку, то, хотя бы в 80%. А это значит, что нормально пользоваться системой без оплаты не получится.

☠️ Реализация проверок выполнения разных действий в разных местах с отдачей соответствующей информации фронту тоже в разных местах. То есть, оплату проверяем в одном месте, и, если ее нет, сообщаем фронту каким-нибудь полем в ответе, а заполненность профиля проверяем в другом месте. Такой подход размазывает логику как на бэке, так и на фронте.

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

class UserObligations
{
private string $userId;

private bool $fillProfile = false;

//id модулей системы, для которых нужно принять соглашение
private array $acceptAgreementForUnits = [];

//...
}


Дальше алгоритм действий простой:

1️⃣ При каждом запросе получить залогиненного юзера и проверить его обязательства до выполнения контроллера. Как правило, фреймворки предоставляют удобные возможности сделать это. Детали реализации зависят уже от вашего фреймворка и того, как вы им пользуетесь. Можно, например, включать информацию об обязательствах в JWT (см., например, мою статью про JWT).

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

3️⃣ Фронт на своей стороне знает все значения enum обязательных действий и соответствующие им страницы для редиректа.

4️⃣ При возникновении обязательства пользователя необходимо обновлять объект UserObligations. Например, при создании пользователя добавлять ему обязательство заполнить профиль, а при пересчете баланса и уходе его в ноль добавлять обязательство оплаты.

5️⃣ Соответственно, при выполнении обязательства также необходимо обновлять объект UserObligations. Заполнил профиль - сняли обязательство. Пополнил баланс - аналогично.

При таком подходе

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

☘️ Легче читать и расширять код, повышается cohesion.

☘️ Обойти выполнение обязательства нельзя: в ответ на любой запрос бэк будет сообщать о необходимости выполнить действие и не будет обрабатывать запрос.

☘️ Даже если у юзера будет больше одного обязательства, бэк последовательно и понятно обработает их все, выдавая требования по одному. Например, юзер логинится в систему и вместо дашборда получает редирект на страницу заполнения профиля. Заполнив профиль, юзер, если у него есть второе обязательство, будет еще раз редиректнут, но уже на страницу оплаты. И так до тех пор, пока все обязательства не будут выполнены.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍61🔥1
Немного про cohesion. Запихиваем кишки класса обратно в класс.

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

class TaxationSettings
{
/** @var TariffSettings[] */
public array $tariffs;

/** @var ServiceSettings[] */
public array $services;
}

class GetTaxationSettingsHandler
{
public function handle(string $hotelId): TaxationSettings
{
$result = new TaxationSettings();
$tariffs = $this->tariffRepository->getForHotel($hotelId);
$services = $this->serviceRepository->getForHotel($hotelId);

$result->tariffs = array_map(
fn(Tariff $t) => new TariffSettings(/*создаем из $t*/),
$tariffs
);

$result->services = array_map(
fn(Service $s) => new ServiceSettings(/*создаем из $s*/),
$services
);
}
}


Такой код понижает cohesion, потому что:

☠️ Знание о том, как именно создавать инстансы TariffSettings и ServiceSettings лучше поместить либо прямо в эти классы, либо в агрегирующий их TaxationSettings. Представьте, что вам еще где-то по коду надо будет создать TaxationSettings. Возникнет дублирование кода с логикой создания.

☠️ Из-за того, что массивы $tariffs и $services создаются в клиентском коде, класс TaxationSettings не знает, как именно эти массивы структурированы, и не управляет их содержимым.

Представьте, что нам понадобится получать из TaxationSettings настройки налогообложения по id или названию тарифа и также по id или названию сервиса. Поскольку сам TaxationSettings не знает, что у него внутри, нам везде по клиентскому коду придется вытаскивать весь массив TariffSettings или ServiceSettings и обходить его циклом в поисках нужного нам элемента.

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

Чтобы повысить cohesion, мы можем переписать этот код таким образом:

class TaxationSettings
{
/** @var TariffSettings[] key - tariffId */
private array $tariffs;

/** @var ServiceSettings[] key - serviceId */
private array $services;

public function addTariff(Tariff $tariff)
{
$this->tariffs[$tariff->getId()] = new TariffSettings(/*...*/);
}

public function tariffVatById(int $tariffId): int
{
if (!isset($this->tariffs[$tariffId])) {
throw new \RuntimeException();
}

return $this->tariffs[$tariffId]->vatRate;
}
}


Аналогичные методы можно сделать и для $services. Если нужен поиск по имени тарифа, то мы можем добавить поле $tariffsByNames и хранить внутри TaxationSettings настройки налогов сразу по id и по имени.

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


Такой подход не только повышает cohesion, но и соблюдает инкапсуляцию. Ведь класс - это поведение и данные, инкапсулированные за интерфейсом. Не нужно вытаскивать сырые данные из класса в клиентский код (разматывать ему кишки).

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

#cohesion
👍4
Немного о выгорании и восстановлении

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

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

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

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

🍀 Полное восстановление после выгорания и более тяжелой формы - депрессии - может занимать от месяца до полутора лет.

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

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

🍀 Оказывается, среди людей, склонных строить сложные планы и ставить высокие планки, распространена такая проблема: они не верят, что простые активности, типа прогулки или прослушивания музыки, могут быть полноценным отдыхом и всерьез способствовать восстановлению, помогая в итоге достичь основной цели. А они могут.

Итого

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

Но чем больше с годами становится мой опыт выгораний, тем чаще я начинаю задумываться: может, все наоборот? Если выстроить правильный отдых, то работа сама пойдет?
Please open Telegram to view this post
VIEW IN TELEGRAM
1🔥1🙏1
В общем, сел я писать пост про анемичные модели и, как всегда, разросся он у меня до таких размеров, что в пору делить на три части. Решил опубликовать как пост на Хабре, но и в ограничения хабропоста этот текст не влез.((

Короче, плюнул и опубликовал как статью. На мой вкус для статьи маловато, конечно, но, подождем реакции публики.)) Особенно из хаба "Проектирование и рефакторинг". Они там любят погнобить конструктивную критику. 😄

https://habr.com/ru/articles/919198/
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5👏2
🔒Набор пока завершен. Группа из пяти человек набралась на удивление быстро!)) Если будет донабор (а я надеюсь, что он обязательно будет), то я об этом напишу отдельным постом.

💡 Давно вынашиваю пару идей, которые, наконец, соединились в одном эксперименте. Приглашаю к участию всех желающих.)

🌳 Идея номер раз. Построить большое дерево навыков PHP-разработчика, по которому видно весь ландшафт: что нужно уметь, куда прокачиваться и как оценивать свои компетенции.

💬 Идея номер два. Надоело выходить на команды с докладами по собственной инициативе и на темы, которые сам подобрал. Тут хочется двух вещей: запроса от слушателей и обратной связи.

Так что, запускаю по такому поводу эксперимент под рабочим названием «Клуб прокачки».)) Коротко это выглядит так:

🍒 Набираем группу до 5 человек,

🍒 Каждую неделю-две выбираем один навык из дерева,

🍒 Само дерево сейчас на старте, оно будет расти вместе с клубом. Вы будете влиять на его структуру и содержание.

🍒 По выбранному навыку я готовлю небольшой доклад, возможно задания для самоконтроля.

🍒 Собираемся, слушаем доклад, обсуждаем, задаем вопросы.

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

Подробнее почитать о клубе можно тут.

Кому интересно попробовать, напишите мне в личку «Хочу вступить в клуб».)

Да, забыл написать. Это бесплатно.)

#LevelUp
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2
Вчера с командой принимали два командных соглашения: делать доктриновский маппинг через атрибуты или XML, и использовать ли доктриновские релейшены.

Готовясь к вступлению, собрал вот такую табличку плюсов-минусов. Делюсь, может кому-то тоже пригодится.)

⚙️ Атрибуты

Привычный вид
Прямо в коде — кому-то это удобно

👎 Тащим архитектурную зависимость в доменный слой
👎 Концептуально это неверно — смешение данных и метаданных

⚙️ XML

Код сущностей не засоряется
Чистая архитектура: сущности не несут метаданных и инфра-зависимостей
Весь маппинг в одном месте — удобно искать и навигировать

👎 Непривычный формат, но дело привычки

⚙️ Doctrine relations

Удобны на старте проекта
Автоматическая гидрация

👎 Тащат инфраструктурную зависимость в сущности
👎 Затрудняют доработку функционала коллекций
👎 Тяжелы в отладке
👎 Требуют внимание к себе при чуть более сложных кейсах

А вы что используете в своих проектах и какие плюсы и минусы видятся вам?)
Please open Telegram to view this post
VIEW IN TELEGRAM
1👎1🔥1
🎉 Старт нашего LevelUp Club превзошёл все мои ожидания!

Участники не только предложили дополнения к дереву навыков, но и проявили сразу несколько инициатив. 💪 Мне оставалось лишь успевать задавать форму этому потоку.))

В итоге, в Клубе образовалось сразу два пространства.

📚 Библиотека, как более лайтовый формат, где планируется раз в 1-2 недели выбирать навык, по которому я буду готовить небольшое занятие для его прокачки.

⚒️ Кузница, как более вовлекающий и жесткий формат. В Кузнице мы планируем пилить MVP платформы для нашего клуба с аутентификацией и трекингом прогресса по навыкам. Всё, что происходит в Кузнице, тоже строится вокруг обучения и практики.

Двое участников уже заработали первые ачивки Мастера-кузнеца и Вестника-первопроходца. 🙂 А в качестве первого навыка на прокачку Вестник выбрал рефлексию.

Сейчас я готовлю первое занятие в Библиотеке и с энтузиазмом наблюдаю, как участники обустраивают Кузницу и готовятся к работе.

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

#LevelUp
Please open Telegram to view this post
VIEW IN TELEGRAM
👻3🔥2
Базовые принципы CQRS, как спасение от Legacy

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

📖 Немного теории

Если совсем упрощенно и кратко: есть такой архитектурный подход - CQRS. Ключевой его идеей является разделение моделей на модели чтения и модели записи.

А связующим звеном между ними выступают проекции. Проекции "понимают", какие изменения тригерят модели записи и заботятся о том, чтобы внести необходимые изменения в модели чтения.

💻 Пример

Представьте, что есть таблица order на 50 полей, которая хранит в себе все возможные детали заказа. Суть в том, что мы не обязаны делать одну модель с мапингом на всю эту огромную таблицу. Мы можем сделать несколько моделей чтения и несколько моделей записи.

✍️ Модели записи будут отвечать за внесение изменений в данные, причем каждая модель может быть как на все 50 полей таблицы, так и только на 2-3 поля. Моделей этих может быть сколько угодно и они могут смотреть на одни и те же столбцы, это допускается.

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

🔧 Как это помогает бороться с Legacy

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

☘️ Новые модели смотрят на те же данные, но не обязаны описывать все поля. Только те, которые вам необходимы для текущей задачи.

☘️ Нет никаких проблем с тем, чтобы создавать новую модель под каждый новый блок функционала. Например, у вас есть legacy Order на 50 полей. Вы можете создать Order на 7 полей для нужд программы лояльности, Order на 23 поля для нужд нового модуля Склада и т. д.

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

☘️ В новых моделях вы свободны от старого нейминга и неудачных решений. То, что вы используете старые данные, не обязывает вас использовать их по-старому. Поле в таблице и в старой сущности назвали company_order_price_i_like_long_names? Вам никто не мешает в новом классе сделать просто поле price. Цена хранится в float, а вы давно хотите перейти на копейки в int? Назовите поле priceKopecks и делайте (int)ceil($data['company_order_price']) при гидрации.

☘️ Вы можете задеприкейтить старую модель и больше никогда в нее не дописывать код.

☘️ Старая модель как бы навязывает вам необходимость все писать в одном месте из-за своей гипер-ответственности ("Божий класс"). Переход на модели-проекции позволяет вам отвязаться от этой единой точки. Order для нужд склада умеет что-то только про склад, а Order для нужд бонусной системы знает что-то, что нужно только ему. Да, у вас много классов Order, это вам навязал legacy, но теперь вы не обязаны лепить весь код в одно место.
6🔥6👍2
Решил в субботу наделать крафтовых сосисок. Сейчас отварил пару на обед. Жена, увидев сей кулинарно-эротический шедевр, объявила, что не будет это есть. А зря. На вкус как домашние пельмешки без теста, только нежнее. Ням ням.
😁7👏5🥰1
Проработав в десятке команд, я заметил, что многие разработчики плохо понимают разницу между такими вещами, как событие, сообщение и транспорт. Уже много лет хотел написать подобную статью. Очень рекомендую.

https://habr.com/ru/articles/921656/
🔥10👍41
Почему я не люблю DRY

Отвечаю на вопрос подписчика к посту о CQRS "а как же DRY?"

Первое, что нужно понять: не следует понимать все слишком буквально и прямолинейно. Второе: невозможно писать код, соответствующий всем принципам, правилам, парадигмам и best practices.

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


Принципы, паттерны, архитектурные подходы – это больше про мышление, чем про дотошное исполнение в любой ситуации.

🔎 Давайте сначала немножечко о DRY

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

Везде ли надо его натягивать?

DRY гласит, что всякое знание в системе должно иметь единственное представление.

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

Да, когда вы выделяете три класса: request, entity и DTO, значительная часть полей в этих классах может совпадать, и изменение entity потянет за собой изменение request и DTO.

Является ли это нарушением DRY?

Мое мнение: вопрос не имеет смысла.

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

Я скажу: request, entity и DTO – три разные единицы, они про разное. И буду прав. Вы скажете: request и DTO – производные от entity, и первоисточник знаний – entity. И будете правы. А потом придет третий чувак и спросит: а как же Single responsibility?

Качество кода в подходе с выделением request, entity и DTO значительно выше, чем при соединении всего в одном классе. И для меня это – ключевой критерий.

📌 Теперь про вопрос с legacy Order и его проекциями

Вопрос подписчика звучал так:

Например, изменять количество можно только пока заказ не оплачен. Менять количество можно как в классе модуля заказов, так и в классе модуля склада. Получается и там и там нужно проверять статус заказа, сама проверка и есть DRY. Если появится новый статус, то надо не забыть его обработать в обоих классах.


Отвечаю по порядку с учетом написанного выше.

🧠 Вы сами решаете, что для вас больнее: продолжать работать с огромным legacy классом или нарушить DRY. И это не ирония и не колкость. Вы принимаете решение, как инженер, с учетом условий вашего проекта.

В заскорузлом legacy идеальных решений нет. Только боль и компромиссы.


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

Дотошное следование DRY почти всегда ведет к переусложнению.


🧠 Надо понимать, что ваш код – это воплощение вашего образа мышления и вашего видения. «Изменять количество можно только пока заказ не оплачен». Это один инвариант, или два? А если завтра бузинес скажет вам: мы хотим иметь возможность сделать частичный возврат средств покупателю если при сборке оплаченного заказа товара на складе оказалось недостаточно. В этой ситуации вам будет удобнее с одним инвариантом или с двумя?

Мысль, которую я пытаюсь донести, следующая:

DRY слишком универсален, чтобы применять его дотошно к любой ситуации. Только вы решаете, где у вас нарушение, а где – нет.


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

#DRY
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🔥51
🤔 Мысль за обедом

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

Представьте дядю Васю, задумчиво почесывающего щетину на подбородке и дымящего Беломором, которому молодой племянник объявил, что хочет стать мЕнеджером или «айтишником».

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

Прошло 20 лет, и наш воображаемый дядя Вася ни то, что не помер, но, возможно, даже и на пенсию еще не вышел. Жизнь уже научила его, что некоторые мЕнеджеры за год зарабатывают больше, чем он за всю жизнь, но он все равно мыслит с привычной инерцией и верит, что обладание навыком загуглить – уже неплохое достижение в мире Интернета.

А вот теперь представьте, как к этому же дяде подходит внук и объявляет, что хочет стать… Оркестратором смыслов. Потому что в мире AI традиционные профессии просто вымрут. Как вы думаете, дядя Вася застрелится, или просто напьется?

А мысли все эти пошли у меня оттуда, что раньше нам в школе говорили: «мы не учим вас будущей профессии, мы учим вас учиться». Похоже, что современная школа моего ребенка не научит даже этому.
👍61
🔒 Набор завершен.

📯 Объявляется донабор в наш LevelUp Club

Двери открыты для первых пяти желающих.) В Клубе проводятся занятия в двух пространствах: Библиотеки и Кузницы.

📜 В Библиотеке мы работаем над созданием большого дерева навыков PHP-разработчика, по которому видно весь ландшафт: что нужно уметь, куда прокачиваться и как оценивать свои компетенции. Участники сами выбирают навык из дерева, а я провожу по этому навыку занятие.

⚒️ В Кузнице мы планируем совместную разработку платформы Клуба в обучающих целях. На занятиях я планирую делиться своим подходом к разработке.

Первое занятие в Библиотеке уже в это воскресенье.

Подробнее можно почитать в правилах Клуба.

Чтобы присоединиться, просто напишите мне в личку @slayervc со словами "хочу вступить в Клуб". 🙂

Участие бесплатное.)

#LevelUp
Please open Telegram to view this post
VIEW IN TELEGRAM
🕊4👍1👏1
Принципы построения хорошего репозитория

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

Прежде чем их перечислить, напомню: репозиторий по своей сути - это коллекция объектов, из которой можно что-то достать, и в которую можно что-то положить.

Это не коннект к базе, не запрос, не QueryBuilder и т. п.


Все перечисленное - детали реализации. О репозитории всегда нужно думать как о коллекции объектов. Представляйте, что ваш репозиторий держит в памяти PHP процесса все объекты, которые могут быть из него извлечены. И проектируйте его интерфейс исходя из этого.

🔁 Всегда следуйте принципу инвертирования зависимостей

Какой бы репозиторий или Storage вы не писали, всегда прокладывайте интерфейс между вашим репозиторием и клиентским кодом. Это позволит вам:

☘️ Спокойно писать бизнес-логику, откладывая детали выборки данных из базы на последний момент.

☘️ Выделить реализацию репы в отдельную задачу и отдать ее коллеге. 😄

☘️ В будущем легко добавлять новые реализации при переходе на другое хранилище (редко, но бывает), фреймворк (актуально в легаси проектах) или микросервисы (вместо базы репа дергает API).

🗃 Используйте классы коллекций вместо plural types

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

🚫 Не держите даже примитивную логику в репозитории

Вот пример нарушения этого принципа:

class UserRepository implements UserRepositoryInterface
{
public function deleteWhoFinishedCourse():void
{
//Удалить всех пользователей, кто привязан к курсу со статусом FINISHED

$userIds = $this->em->createQueryBuilder()->... //Делаем сложный запрос для выборки id
foreach ($userIds as $id) {
$this->em->createQueryBuilder->()... //Делаем запрос на удаление по id
}
}
}


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

Правильнее было бы выделить в репозитории два метода: один для выборки юзеров (универсальный на насколько случаев, или конкретно для этой задачи), второй - для удаления по id. А формирование самих критериев удаления по случаю завершения курса лучше вынести в слой бизнес-логики, или, в крайнем случае, в Application.

✂️ Репозитории можно делить не только по сущностям, но и на Read и Write

Эта идея позаимствована из CQRS. Если ваш репозиторий слишком большой, или если вы явно следуете CQRS, то выделение UserReadRepository и UserWriteRepository будет вам полезно.

🪶 Переход на абстракции не обязывает вас отказываться от ORM и даже ActiveRecord

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

☘️ Сначала вы можете написать InMemoryRepository для тестов.

☘️ Потом вы можете написать ActiveRecordRepository в качестве первой быстрой реализации. Внутри можно прямо дергать методы ActiveRecord, это ничему не мешает, ведь вы отделены от остального кода интерфейсом.

☘️ Позднее вы можете решить притащить Doctrine и написать DoctrineRepository, где спокойно можете использовать EntityManager.

☘️ И, наконец, столкнувшись с высокой нагрузкой вы можете захотеть написать PDORepository или DBALRepository с запросами на нативном SQL и ручной гидрацией.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9👍3🤩21