Механизм обязательств пользователя
Простая, вроде бы, вещь, но, почему-то ни в одном виденном мною проекте этот механизм не был реализован. Речь идет о ситуации, когда от пользователя требуется совершить какое-то действие до того, как он получит доступ к интерфейсу. Чаще всего это:
🔒 Заполнение личного профиля или профиля компании.
🔒 Внесение оплаты за пользование системой.
🔒 Принятие пользовательского соглашения.
Как привило, с точки зрения бизнес-задачи и пользовательского опыта требуется такая реализация этого механизма, которая:
☘️ Не позволит обойти выполнение действия, например, перейдя по прямому URL в какой-нибудь подраздел нашей системы.
☘️ Будет предсказуемо редиректить пользователя на соответствующую страницу, на которой он сможет выполнить требующееся от него действие. Причем, для каждого действия мы хотели бы видеть отдельную страницу: свою - для оплаты, свою - для профиля и т. д.
Основные ошибки в реализации этой задачи, которые я видел от проекта к проекту, выглядят так:
☠️ Вынесение логики с проверкой выполнения действия на фронт.
☠️ Привязка проверки выполнения действия к какому-либо другому действию. Например, при загрузке профиля компании проверяем оплату. Предполагается, что профиль компании загружается если и не в 100% обращений к бэку, то, хотя бы в 80%. А это значит, что нормально пользоваться системой без оплаты не получится.
☠️ Реализация проверок выполнения разных действий в разных местах с отдачей соответствующей информации фронту тоже в разных местах. То есть, оплату проверяем в одном месте, и, если ее нет, сообщаем фронту каким-нибудь полем в ответе, а заполненность профиля проверяем в другом месте. Такой подход размазывает логику как на бэке, так и на фронте.
Альтернативой таким решениям может быть вынесение логики проверки выполнения действия в отдельных механизм.
Дальше алгоритм действий простой:
1️⃣ При каждом запросе получить залогиненного юзера и проверить его обязательства до выполнения контроллера. Как правило, фреймворки предоставляют удобные возможности сделать это. Детали реализации зависят уже от вашего фреймворка и того, как вы им пользуетесь. Можно, например, включать информацию об обязательствах в JWT (см., например, мою статью про JWT).
2️⃣ При наличии невыполненного действия сообщить фронту о необходимости редиректа. Сделать это можно с помощью согласованного с фронтом кода ответа и единственного поля, содержащего enum действия. Если действий, требуемых от пользователя, несколько, то сообщить нужно только об одном, например, самом приоритетном.
3️⃣ Фронт на своей стороне знает все значения enum обязательных действий и соответствующие им страницы для редиректа.
4️⃣ При возникновении обязательства пользователя необходимо обновлять объект
5️⃣ Соответственно, при выполнении обязательства также необходимо обновлять объект
При таком подходе
☘️ Вся информация о том, что должен сделать пользователь, собирается в одном классе.
☘️ Легче читать и расширять код, повышается cohesion.
☘️ Обойти выполнение обязательства нельзя: в ответ на любой запрос бэк будет сообщать о необходимости выполнить действие и не будет обрабатывать запрос.
☘️ Даже если у юзера будет больше одного обязательства, бэк последовательно и понятно обработает их все, выдавая требования по одному. Например, юзер логинится в систему и вместо дашборда получает редирект на страницу заполнения профиля. Заполнив профиль, юзер, если у него есть второе обязательство, будет еще раз редиректнут, но уже на страницу оплаты. И так до тех пор, пока все обязательства не будут выполнены.
Простая, вроде бы, вещь, но, почему-то ни в одном виденном мною проекте этот механизм не был реализован. Речь идет о ситуации, когда от пользователя требуется совершить какое-то действие до того, как он получит доступ к интерфейсу. Чаще всего это:
🔒 Заполнение личного профиля или профиля компании.
🔒 Внесение оплаты за пользование системой.
🔒 Принятие пользовательского соглашения.
Как привило, с точки зрения бизнес-задачи и пользовательского опыта требуется такая реализация этого механизма, которая:
☘️ Не позволит обойти выполнение действия, например, перейдя по прямому URL в какой-нибудь подраздел нашей системы.
☘️ Будет предсказуемо редиректить пользователя на соответствующую страницу, на которой он сможет выполнить требующееся от него действие. Причем, для каждого действия мы хотели бы видеть отдельную страницу: свою - для оплаты, свою - для профиля и т. д.
Основные ошибки в реализации этой задачи, которые я видел от проекта к проекту, выглядят так:
☠️ Вынесение логики с проверкой выполнения действия на фронт.
☠️ Привязка проверки выполнения действия к какому-либо другому действию. Например, при загрузке профиля компании проверяем оплату. Предполагается, что профиль компании загружается если и не в 100% обращений к бэку, то, хотя бы в 80%. А это значит, что нормально пользоваться системой без оплаты не получится.
☠️ Реализация проверок выполнения разных действий в разных местах с отдачей соответствующей информации фронту тоже в разных местах. То есть, оплату проверяем в одном месте, и, если ее нет, сообщаем фронту каким-нибудь полем в ответе, а заполненность профиля проверяем в другом месте. Такой подход размазывает логику как на бэке, так и на фронте.
Альтернативой таким решениям может быть вынесение логики проверки выполнения действия в отдельных механизм.
class UserObligations
{
private string $userId;
private bool $fillProfile = false;
//id модулей системы, для которых нужно принять соглашение
private array $acceptAgreementForUnits = [];
//...
}
Дальше алгоритм действий простой:
UserObligations. Например, при создании пользователя добавлять ему обязательство заполнить профиль, а при пересчете баланса и уходе его в ноль добавлять обязательство оплаты.UserObligations. Заполнил профиль - сняли обязательство. Пополнил баланс - аналогично.При таком подходе
☘️ Вся информация о том, что должен сделать пользователь, собирается в одном классе.
☘️ Легче читать и расширять код, повышается cohesion.
☘️ Обойти выполнение обязательства нельзя: в ответ на любой запрос бэк будет сообщать о необходимости выполнить действие и не будет обрабатывать запрос.
☘️ Даже если у юзера будет больше одного обязательства, бэк последовательно и понятно обработает их все, выдавая требования по одному. Например, юзер логинится в систему и вместо дашборда получает редирект на страницу заполнения профиля. Заполнив профиль, юзер, если у него есть второе обязательство, будет еще раз редиректнут, но уже на страницу оплаты. И так до тех пор, пока все обязательства не будут выполнены.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤1🔥1
Немного про cohesion. Запихиваем кишки класса обратно в класс.
Cohesion - это то, насколько код сбит в кучу, насколько он сфокусирован и держит форму, не растекается по кодовой базе. Давайте рассмотрим такой код:
Такой код понижает cohesion, потому что:
☠️ Знание о том, как именно создавать инстансы
☠️ Из-за того, что массивы
Представьте, что нам понадобится получать из
Больше того, не будет лишним проводить каждый раз проверку, что вытащенные из
Чтобы повысить cohesion, мы можем переписать этот код таким образом:
Аналогичные методы можно сделать и для
Такой подход не только повышает cohesion, но и соблюдает инкапсуляцию. Ведь класс - это поведение и данные, инкапсулированные за интерфейсом. Не нужно вытаскивать сырые данные из класса в клиентский код (разматывать ему кишки).
Оставьте всю техническую логику внутри класса и ваш клиентский код станет чистым и лаконичным, а дальнейшая доработка этой логики потребует изменений всего в одном классе, который ее содержит, вместо многочисленных изменений задублированных фрагментов по всему клиентскому коду.
#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 подзадач мега-цель по захвату мира должна включать такие пункты, как полежать на диване с массажером, или заварить чай и неспеша выпить его в уютной атмосфере. Я серьезно.
🍀 Оказывается, среди людей, склонных строить сложные планы и ставить высокие планки, распространена такая проблема: они не верят, что простые активности, типа прогулки или прослушивания музыки, могут быть полноценным отдыхом и всерьез способствовать восстановлению, помогая в итоге достичь основной цели. А они могут.
Итого
Не знаю как для вас, а для меня необходимость такого серьезного отношения к восстановлению оказалась неожиданностью. Вроде как, делу - время, а потехе - час. Я всегда считал, что главное - заставить себя работать, а отдых сам собой получится.
Но чем больше с годами становится мой опыт выгораний, тем чаще я начинаю задумываться: может, все наоборот? Если выстроить правильный отдых, то работа сама пойдет?
Столкнулся тут с проблемой, когда в понедельник отрабатываешь полный день, делаешь еще доп. активность, идешь в зал и потом еще что-то по дому успеваешь. Нормальный такой продуктивный день.
Но со вторника обнаруживаешь, что сил едва хватает даже на основную работу. В итоге до выходных доходишь кое-как, неделя в говно с точки зрения результата, а самое главное, непонятно, почему.
Такой bullshit повторялся из недели в неделю, пока я, наконец, не решил погрузиться в проблему, в попытках понять, что происходит, и почему. Не буду утомлять вас всеми деталями своих изысканий, а поделюсь только некоторыми конечными выводами из них.
Итого
Не знаю как для вас, а для меня необходимость такого серьезного отношения к восстановлению оказалась неожиданностью. Вроде как, делу - время, а потехе - час. Я всегда считал, что главное - заставить себя работать, а отдых сам собой получится.
Но чем больше с годами становится мой опыт выгораний, тем чаще я начинаю задумываться: может, все наоборот? Если выстроить правильный отдых, то работа сама пойдет?
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1🔥1🙏1
В общем, сел я писать пост про анемичные модели и, как всегда, разросся он у меня до таких размеров, что в пору делить на три части. Решил опубликовать как пост на Хабре, но и в ограничения хабропоста этот текст не влез.((
Короче, плюнул и опубликовал как статью. На мой вкус для статьи маловато, конечно, но, подождем реакции публики.)) Особенно из хаба "Проектирование и рефакторинг". Они там любятпогнобить конструктивную критику. 😄
https://habr.com/ru/articles/919198/
Короче, плюнул и опубликовал как статью. На мой вкус для статьи маловато, конечно, но, подождем реакции публики.)) Особенно из хаба "Проектирование и рефакторинг". Они там любят
https://habr.com/ru/articles/919198/
Please open Telegram to view this post
VIEW IN TELEGRAM
Хабр
Анемичные модели с логикой в сервисах: плюсы и минусы одного из самых популярных подходов к разработке на PHP
Недавно состоялся у меня небольшой спор с коллегой, активно защищающим анемичные модели с размещением логики в сервисах. Несколько лет назад я и сам был последовательным фанатом такой...
🔥5👏2
🔒Набор пока завершен. Группа из пяти человек набралась на удивление быстро!)) Если будет донабор (а я надеюсь, что он обязательно будет), то я об этом напишу отдельным постом.
💡 Давно вынашиваю пару идей, которые, наконец, соединились в одном эксперименте. Приглашаю к участию всех желающих.)
🌳 Идея номер раз. Построить большое дерево навыков PHP-разработчика, по которому видно весь ландшафт: что нужно уметь, куда прокачиваться и как оценивать свои компетенции.
💬 Идея номер два. Надоело выходить на команды с докладами по собственной инициативе и на темы, которые сам подобрал. Тут хочется двух вещей: запроса от слушателей и обратной связи.
Так что, запускаю по такому поводу эксперимент под рабочим названием «Клуб прокачки».)) Коротко это выглядит так:
🍒 Набираем группу до 5 человек,
🍒 Каждую неделю-две выбираем один навык из дерева,
🍒 Само дерево сейчас на старте, оно будет расти вместе с клубом. Вы будете влиять на его структуру и содержание.
🍒 По выбранному навыку я готовлю небольшой доклад, возможно задания для самоконтроля.
🍒 Собираемся, слушаем доклад, обсуждаем, задаем вопросы.
🍒 Если эксперимент пойдет, то в планах игрофикация, ачивки, ранги и различные групповые активности.
Подробнее почитать о клубе можно тут.
Кому интересно попробовать, напишите мне в личку «Хочу вступить в клуб».)
Да, забыл написать. Это бесплатно.)
#LevelUp
Так что, запускаю по такому поводу эксперимент под рабочим названием «Клуб прокачки».)) Коротко это выглядит так:
Подробнее почитать о клубе можно тут.
Кому интересно попробовать, напишите мне в личку «Хочу вступить в клуб».)
Да, забыл написать. Это бесплатно.)
#LevelUp
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2
Вчера с командой принимали два командных соглашения: делать доктриновский маппинг через атрибуты или XML, и использовать ли доктриновские релейшены.
Готовясь к вступлению, собрал вот такую табличку плюсов-минусов. Делюсь, может кому-то тоже пригодится.)
⚙️ Атрибуты
➕ Привычный вид
➕ Прямо в коде — кому-то это удобно
👎 Тащим архитектурную зависимость в доменный слой
👎 Концептуально это неверно — смешение данных и метаданных
⚙️ XML
➕ Код сущностей не засоряется
➕ Чистая архитектура: сущности не несут метаданных и инфра-зависимостей
➕ Весь маппинг в одном месте — удобно искать и навигировать
👎 Непривычный формат, но дело привычки
⚙️ Doctrine relations
➕ Удобны на старте проекта
➕ Автоматическая гидрация
👎 Тащат инфраструктурную зависимость в сущности
👎 Затрудняют доработку функционала коллекций
👎 Тяжелы в отладке
👎 Требуют внимание к себе при чуть более сложных кейсах
А вы что используете в своих проектах и какие плюсы и минусы видятся вам?)
Готовясь к вступлению, собрал вот такую табличку плюсов-минусов. Делюсь, может кому-то тоже пригодится.)
⚙️ Атрибуты
⚙️ XML
⚙️ Doctrine relations
А вы что используете в своих проектах и какие плюсы и минусы видятся вам?)
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1👎1🔥1
Участники не только предложили дополнения к дереву навыков, но и проявили сразу несколько инициатив. 💪 Мне оставалось лишь успевать задавать форму этому потоку.))
В итоге, в Клубе образовалось сразу два пространства.
📚 Библиотека, как более лайтовый формат, где планируется раз в 1-2 недели выбирать навык, по которому я буду готовить небольшое занятие для его прокачки.
⚒️ Кузница, как более вовлекающий и жесткий формат. В Кузнице мы планируем пилить MVP платформы для нашего клуба с аутентификацией и трекингом прогресса по навыкам. Всё, что происходит в Кузнице, тоже строится вокруг обучения и практики.
Двое участников уже заработали первые ачивки Мастера-кузнеца и Вестника-первопроходца.
Сейчас я готовлю первое занятие в Библиотеке и с энтузиазмом наблюдаю, как участники обустраивают Кузницу и готовятся к работе.
В общем, движха прет, надеюсь, этот порыв мы не растеряем и через неделю мне будет еще о чем вам рассказать.))
#LevelUp
Please open Telegram to view this post
VIEW IN TELEGRAM
👻3🔥2
Базовые принципы CQRS, как спасение от Legacy
Чем старше кодовая база и чем ниже ее качество, тем дороже становится написание нового функционала. Одним из отличных инструментов преодоления этой проблемы является применение базовых принципов CQRS.
📖 Немного теории
Если совсем упрощенно и кратко: есть такой архитектурный подход - CQRS. Ключевой его идеей является разделение моделей на модели чтения и модели записи.
А связующим звеном между ними выступают проекции. Проекции "понимают", какие изменения тригерят модели записи и заботятся о том, чтобы внести необходимые изменения в модели чтения.
💻 Пример
Представьте, что есть таблица
✍️ Модели записи будут отвечать за внесение изменений в данные, причем каждая модель может быть как на все 50 полей таблицы, так и только на 2-3 поля. Моделей этих может быть сколько угодно и они могут смотреть на одни и те же столбцы, это допускается.
🧾 Модели чтения - это, по сути, вьюшки, представления. Опять же, их может быть сколько угодно и они могут мапиться хоть на два, хоть на все пятьдесят столбцов, и при этом могут пересекаться между собой по столбцам.
🔧 Как это помогает бороться с Legacy
Если в вашей кодовой базе есть старые модели на кучу полей с запутанной логикой, которые страшно трогать, и вам при этом нужно написать новый функционал, опирающийся на эту модель, то вы можете вместо изменения ее кода написать новые классы.
☘️ Новые модели смотрят на те же данные, но не обязаны описывать все поля. Только те, которые вам необходимы для текущей задачи.
☘️ Нет никаких проблем с тем, чтобы создавать новую модель под каждый новый блок функционала. Например, у вас есть legacy
☘️ Вы все равно не будете трогать старые поля вашего 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/
https://habr.com/ru/articles/921656/
Хабр
События vs сообщения. Понимаете ли вы разницу и почему это важно?
«Будем отправлять события в Rabbit!» Фраза, которая выдает мышление, рождающее код, полный боли. К сожалению, я ее часто слышу. Поэтому, уже много лет размышлял о написании этой статьи...
🔥10👍4❤1
Почему я не люблю DRY
Отвечаю на вопрос подписчика к посту о CQRS "а как же DRY?"
Первое, что нужно понять: не следует понимать все слишком буквально и прямолинейно. Второе: невозможно писать код, соответствующий всем принципам, правилам, парадигмам и best practices.
Принципы, паттерны, архитектурные подходы – это больше про мышление, чем про дотошное исполнение в любой ситуации.
🔎 Давайте сначала немножечко о DRY
Этот принцип сформулирован предельно универсально, а значит может быть натянут на любую ситуацию.
❓ Везде ли надо его натягивать?
DRY гласит, что всякое знание в системе должно иметь единственное представление.
Самый часто мне встречавшийся случай гипертрофированного применения DRY – это использование одного класса в качестве риквеста, сущности и DTO. При таком подходе связанность кода становится максимальной, как и число болей по сопровождению проекта.
Да, когда вы выделяете три класса:
❓ Является ли это нарушением DRY?
Мое мнение: вопрос не имеет смысла.
Можно до бесконечности спорить на тему, а что в этом кейсе та самая единица знания, которая не должна повторяться.
Я скажу:
Качество кода в подходе с выделением
📌 Теперь про вопрос с legacy Order и его проекциями
Вопрос подписчика звучал так:
Отвечаю по порядку с учетом написанного выше.
🧠 Вы сами решаете, что для вас больнее: продолжать работать с огромным legacy классом или нарушить DRY. И это не ирония и не колкость. Вы принимаете решение, как инженер, с учетом условий вашего проекта.
🧠 Опция с выносом общих для разных модулей инвариантов в отдельные классы допустима, если вы сумеете красиво ее реализовать. Скорее всего, это будет экзотика и переусложнение кода. Вообще
🧠 Надо понимать, что ваш код – это воплощение вашего образа мышления и вашего видения. «Изменять количество можно только пока заказ не оплачен». Это один инвариант, или два? А если завтра бузинес скажет вам: мы хотим иметь возможность сделать частичный возврат средств покупателю если при сборке оплаченного заказа товара на складе оказалось недостаточно. В этой ситуации вам будет удобнее с одним инвариантом или с двумя?
Мысль, которую я пытаюсь донести, следующая:
Конечно, если у вас появились большие куски повторяемого кода, которые вы меняете одновременно – это явный повод для рефакторинга. Но в части проектирования системы лучше руководствоваться более тонкими подходами.
#DRY
Отвечаю на вопрос подписчика к посту о CQRS "а как же DRY?"
Первое, что нужно понять: не следует понимать все слишком буквально и прямолинейно. Второе: невозможно писать код, соответствующий всем принципам, правилам, парадигмам и best practices.
Решение, которое вы строите - это всегда компромисс с учетом реальных условий проекта.
Принципы, паттерны, архитектурные подходы – это больше про мышление, чем про дотошное исполнение в любой ситуации.
🔎 Давайте сначала немножечко о DRY
Этот принцип сформулирован предельно универсально, а значит может быть натянут на любую ситуацию.
DRY гласит, что всякое знание в системе должно иметь единственное представление.
Самый часто мне встречавшийся случай гипертрофированного применения DRY – это использование одного класса в качестве риквеста, сущности и DTO. При таком подходе связанность кода становится максимальной, как и число болей по сопровождению проекта.
Да, когда вы выделяете три класса:
request, entity и DTO, значительная часть полей в этих классах может совпадать, и изменение entity потянет за собой изменение request и DTO.Мое мнение: вопрос не имеет смысла.
Можно до бесконечности спорить на тему, а что в этом кейсе та самая единица знания, которая не должна повторяться.
Я скажу:
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🔥5❤1
🤔 Мысль за обедом
20 лет назад, когда мое поколение выбирало будущую профессию через выбор вузовской специальности, многие названия казались нашим старшим нелепыми.
Представьте дядю Васю, задумчиво почесывающего щетину на подбородке и дымящего Беломором, которому молодой племянник объявил, что хочет стать мЕнеджером или «айтишником».
Скорее всего, дядя Вася по такому поводу отпустил бы матерную тираду, суть которой сводилась бы к тому, что ерунда все эти ваши новомодные штучки: мЕнеджеров сейчас будет как собак нерезанных, кому они нужны, и, если хочешь быть человеком, то лучше получить хорошую инженерную специальность и работать в строительстве или на производстве.
Прошло 20 лет, и наш воображаемый дядя Вася ни то, что не помер, но, возможно, даже и на пенсию еще не вышел. Жизнь уже научила его, что некоторые мЕнеджеры за год зарабатывают больше, чем он за всю жизнь, но он все равно мыслит с привычной инерцией и верит, что обладание навыком загуглить – уже неплохое достижение в мире Интернета.
А вот теперь представьте, как к этому же дяде подходит внук и объявляет, что хочет стать… Оркестратором смыслов. Потому что в мире AI традиционные профессии просто вымрут. Как вы думаете, дядя Вася застрелится, или просто напьется?
А мысли все эти пошли у меня оттуда, что раньше нам в школе говорили: «мы не учим вас будущей профессии, мы учим вас учиться». Похоже, что современная школа моего ребенка не научит даже этому.
20 лет назад, когда мое поколение выбирало будущую профессию через выбор вузовской специальности, многие названия казались нашим старшим нелепыми.
Представьте дядю Васю, задумчиво почесывающего щетину на подбородке и дымящего Беломором, которому молодой племянник объявил, что хочет стать мЕнеджером или «айтишником».
Скорее всего, дядя Вася по такому поводу отпустил бы матерную тираду, суть которой сводилась бы к тому, что ерунда все эти ваши новомодные штучки: мЕнеджеров сейчас будет как собак нерезанных, кому они нужны, и, если хочешь быть человеком, то лучше получить хорошую инженерную специальность и работать в строительстве или на производстве.
Прошло 20 лет, и наш воображаемый дядя Вася ни то, что не помер, но, возможно, даже и на пенсию еще не вышел. Жизнь уже научила его, что некоторые мЕнеджеры за год зарабатывают больше, чем он за всю жизнь, но он все равно мыслит с привычной инерцией и верит, что обладание навыком загуглить – уже неплохое достижение в мире Интернета.
А вот теперь представьте, как к этому же дяде подходит внук и объявляет, что хочет стать… Оркестратором смыслов. Потому что в мире AI традиционные профессии просто вымрут. Как вы думаете, дядя Вася застрелится, или просто напьется?
А мысли все эти пошли у меня оттуда, что раньше нам в школе говорили: «мы не учим вас будущей профессии, мы учим вас учиться». Похоже, что современная школа моего ребенка не научит даже этому.
👍6❤1
🔒 Набор завершен.
📯 Объявляется донабор в наш LevelUp Club
Двери открыты для первых пяти желающих.) В Клубе проводятся занятия в двух пространствах: Библиотеки и Кузницы.
📜 В Библиотеке мы работаем над созданием большого дерева навыков PHP-разработчика, по которому видно весь ландшафт: что нужно уметь, куда прокачиваться и как оценивать свои компетенции. Участники сами выбирают навык из дерева, а я провожу по этому навыку занятие.
⚒️ В Кузнице мы планируем совместную разработку платформы Клуба в обучающих целях. На занятиях я планирую делиться своим подходом к разработке.
Первое занятие в Библиотеке уже в это воскресенье.
Подробнее можно почитать в правилах Клуба.
Чтобы присоединиться, просто напишите мне в личку @slayervc со словами "хочу вступить в Клуб".🙂
Участие бесплатное.)
#LevelUp
📯 Объявляется донабор в наш LevelUp Club
Двери открыты для первых пяти желающих.) В Клубе проводятся занятия в двух пространствах: Библиотеки и Кузницы.
📜 В Библиотеке мы работаем над созданием большого дерева навыков PHP-разработчика, по которому видно весь ландшафт: что нужно уметь, куда прокачиваться и как оценивать свои компетенции. Участники сами выбирают навык из дерева, а я провожу по этому навыку занятие.
⚒️ В Кузнице мы планируем совместную разработку платформы Клуба в обучающих целях. На занятиях я планирую делиться своим подходом к разработке.
Первое занятие в Библиотеке уже в это воскресенье.
Подробнее можно почитать в правилах Клуба.
Чтобы присоединиться, просто напишите мне в личку @slayervc со словами "хочу вступить в Клуб".
Участие бесплатное.)
#LevelUp
Please open Telegram to view this post
VIEW IN TELEGRAM
🕊4👍1👏1
Принципы построения хорошего репозитория
Репозитории приходится писать нам всем и, в принципе, в них нет ничего особенного, но я все же решил собрать в этом посте все моменты, которые, на мой взгляд, делают репозитории наиболее аккуратными и удобными для использования на всем сроке жизни проекта, а не только при решении текущей задачи.
Прежде чем их перечислить, напомню: репозиторий по своей сути - это коллекция объектов, из которой можно что-то достать, и в которую можно что-то положить.
Все перечисленное - детали реализации. О репозитории всегда нужно думать как о коллекции объектов. Представляйте, что ваш репозиторий держит в памяти PHP процесса все объекты, которые могут быть из него извлечены. И проектируйте его интерфейс исходя из этого.
🔁 Всегда следуйте принципу инвертирования зависимостей
Какой бы репозиторий или
☘️ Спокойно писать бизнес-логику, откладывая детали выборки данных из базы на последний момент.
☘️ Выделить реализацию репы в отдельную задачу и отдать ее коллеге.😄
☘️ В будущем легко добавлять новые реализации при переходе на другое хранилище (редко, но бывает), фреймворк (актуально в легаси проектах) или микросервисы (вместо базы репа дергает API).
🗃 Используйте классы коллекций вместо plural types
О классах коллекций я уже писал. Использование их в интерфейсе репозиториев даст вам более выразительный код, контроль типов и всю техническую логику, которую вы могли добавить в коллекции, сразу из репозитория.
🚫 Не держите даже примитивную логику в репозитории
Вот пример нарушения этого принципа:
В примере выше репозиторий содержит знание о критериях, которым должен соответствовать юзер, завершивший обучение и подлежащий удалению. А это уже бизнес-логика.
Правильнее было бы выделить в репозитории два метода: один для выборки юзеров (универсальный на насколько случаев, или конкретно для этой задачи), второй - для удаления по id. А формирование самих критериев удаления по случаю завершения курса лучше вынести в слой бизнес-логики, или, в крайнем случае, в Application.
✂️ Репозитории можно делить не только по сущностям, но и на Read и Write
Эта идея позаимствована из CQRS. Если ваш репозиторий слишком большой, или если вы явно следуете CQRS, то выделение
🪶 Переход на абстракции не обязывает вас отказываться от ORM и даже ActiveRecord
После того, как вы определили интерфейс репозитория, вы можете писать неограниченное количество его реализаций.
☘️ Сначала вы можете написать
☘️ Потом вы можете написать
☘️ Позднее вы можете решить притащить Doctrine и написать
☘️ И, наконец, столкнувшись с высокой нагрузкой вы можете захотеть написать
Репозитории приходится писать нам всем и, в принципе, в них нет ничего особенного, но я все же решил собрать в этом посте все моменты, которые, на мой взгляд, делают репозитории наиболее аккуратными и удобными для использования на всем сроке жизни проекта, а не только при решении текущей задачи.
Прежде чем их перечислить, напомню: репозиторий по своей сути - это коллекция объектов, из которой можно что-то достать, и в которую можно что-то положить.
Это не коннект к базе, не запрос, не 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🤩2❤1
Паттерн Стратегия. Пример применения.
Я уже писал как-то о превратном понимании Фабрики. Стратегия, наверное, второй паттерн по исковерканности понимания его разработчиками. Что только не объявляли при мне реализацией Стратегии: и обычный if-оператор и какие-то инфраструктурные моменты, типа возможности десериализовать в различные классы входящий JSON.
❓ Так что же такое Стратегия?
Чтобы понять Стратегию (как и почти любой другой паттрен 4-х), нужно понимать полиморфизм (вот мой пост об этом). На самом деле, это очень простой паттерн. Давайте разберем на примере.
Нам необходимо реализовать программу лояльности (ПЛ), поддерживающую разные "валюты" (рубли, бонусы, мили) и разные способы расчета начисляемой суммы: от всей стоимости заказа, только от части заказа и т. д.
Помимо логики расчета начислений, реализация ПЛ имеет и много другой логики. Возникает вопрос:
1️⃣ Первый вариант: писать методы с разной логикой прямо в классе ПЛ и потом обвязывать это все кучей if-ов по всему коду. Ведь нам во многих местах нужно будет понимать как "валюту", в которой считается поощрение клиента, так и способ расчета. При такой реализации код будет запутанным и плохо расширяемым.
2️⃣ Второй вариант: использовать наследование. Мы могли бы сделать несколько классов ПЛ. Общую логику, не касающуюся расчета, можно поместить в базовый класс, а уникальную логику - в потомков. У нас могли бы быть такие классы:
☠️ ПЛ с рублями от всей стоимости заказа.
☠️ ПЛ с рублями только за основные услуги.
☠️ ПЛ с бонусами от всей стоимости заказа.
☠️ И так далее.
Чем больше комбинаций "валюта" <-> способ расчета, тем больше классов. А теперь представьте, что следующей задачей вам нужно добавить в ПЛ функционал массовой раздачи карт клиентам. Раздача карт может быть в зависимости от статуса клиента, или от того, сколько товаров он купил за последний год. То есть, похожая с начислением бонусов задача. Что делать? Плодить еще комбинации наследников?
3️⃣ Третий вариант: пойти по принципу "Предпочитайте композицию наследованию". Вынесем логику расчета бонусов и стартовой раздачи карт в отдельные классы. И вот тут-то и будем плодить наследников. А класс ПЛ будет в себя агрегировать конкретные реализации алгоритмов.
Чтобы это работало совсем удобно и красиво, мы объявим базовый интерфейс для класса, рассчитывающего сумму бонусов, и еще один - для класса, раздающего карты. Такой подход позволит нам полиморфно подменять алгоритмы расчета и алгоритмы раздачи карт в нашей ПЛ. Причем, благодаря полиморфизму, мы сможем это делать даже в рантайме.
Вот мы и реализовали паттерн Стратегия. У нас появилась стратегия расчета бонусов и стратегия стартовой выдачи карт. Вот пример кода, а пояснения к нему я дам в следующем посте.⬇️
Я уже писал как-то о превратном понимании Фабрики. Стратегия, наверное, второй паттерн по исковерканности понимания его разработчиками. Что только не объявляли при мне реализацией Стратегии: и обычный if-оператор и какие-то инфраструктурные моменты, типа возможности десериализовать в различные классы входящий JSON.
Чтобы понять Стратегию (как и почти любой другой паттрен 4-х), нужно понимать полиморфизм (вот мой пост об этом). На самом деле, это очень простой паттерн. Давайте разберем на примере.
Нам необходимо реализовать программу лояльности (ПЛ), поддерживающую разные "валюты" (рубли, бонусы, мили) и разные способы расчета начисляемой суммы: от всей стоимости заказа, только от части заказа и т. д.
Помимо логики расчета начислений, реализация ПЛ имеет и много другой логики. Возникает вопрос:
Как реализовать различные варианты расчета бонусов?
☠️ ПЛ с рублями от всей стоимости заказа.
☠️ ПЛ с рублями только за основные услуги.
☠️ ПЛ с бонусами от всей стоимости заказа.
☠️ И так далее.
Чем больше комбинаций "валюта" <-> способ расчета, тем больше классов. А теперь представьте, что следующей задачей вам нужно добавить в ПЛ функционал массовой раздачи карт клиентам. Раздача карт может быть в зависимости от статуса клиента, или от того, сколько товаров он купил за последний год. То есть, похожая с начислением бонусов задача. Что делать? Плодить еще комбинации наследников?
Чтобы это работало совсем удобно и красиво, мы объявим базовый интерфейс для класса, рассчитывающего сумму бонусов, и еще один - для класса, раздающего карты. Такой подход позволит нам полиморфно подменять алгоритмы расчета и алгоритмы раздачи карт в нашей ПЛ. Причем, благодаря полиморфизму, мы сможем это делать даже в рантайме.
Вот мы и реализовали паттерн Стратегия. У нас появилась стратегия расчета бонусов и стратегия стартовой выдачи карт. Вот пример кода, а пояснения к нему я дам в следующем посте.
class LoyaltyProgram
{
private string $id;
//enum {KOPECKS, MILES, BONUSES}
private LoyaltyCurrency $currency;
private AccumulationStrategy $accumulationStrategy;
private InitialCardIssuing $initialCardIssuing;
//Начислить бонусы на карту
public function accrue(Order $order, LoyaltyCard $card): void
{
$amount = $this->accumulationStrategy->calculate($order);
$card->accrue($amount);
//...
}
}
abstract class AccumulationStrategy
{
public const ?LoyaltyCurrency CURRENCY = null;
public function __construct()
{
if (null === static::CURRENCY) {
throw new \LogicException();
}
}
/**
* @return int сумма начислений на карту ПЛ в валюте ПЛ
*/
abstract public function calculate(Order $order): int;
}
class BaseServicesKopecksAccumulationStrategy extends AccumulationStrategy
{
public const ?LoyaltyCurrency CURRENCY = LoyaltyCurrency::KOPECKS;
public function calculate(Order $order): int
{
//...
}
}
class MilesAccumulationStrategy extends AccumulationStrategy
{
//...
}
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👍4
Паттерн Стратегия. Продолжение.
В предыдущем посте⬆️ я показал, как использовать паттерн Стратегия на примере программы лояльности. Давайте теперь поясню этот пример чуть подробнее. Благодаря такой реализации пользователи могут создавать свои программы лояльности с нужной им "валютой" и способом расчета поощрений.
Как видите, в данной конкретной реализации стратегия расчета поощрений хранит в себе знание о том, в какой валюте она рассчитывает накопления, и именно стратегия определяет валюту для программы лояльности. Это частный случай и ваши стратегии вовсе не обязаны как-то влиять на классы, которые их в себя агрегируют.
Что дает нам такое решение:
🍀 Можно писать неограниченное количество стратегий, не меняя код
🍀 Можно спокойно расширять список "валют" (рубли, бонусы, мили, что угодно).
🍀
Как видите, Стратегия довольно проста и сводится к тому, чтобы вынести за общий интерфейс семейство алгоритмов (расчета поощрений, стартовой выдачи карт и т. д.)
Вообще Стратегия - один из самых простых и понятных способов создать точку расширения в вашем коде, избегая кучи if-ов и запутывания логики.
В предыдущем посте
class LoyaltyProgram
{
//...
public function changeAccumulationStrategy(AccumulationStrategy $strategy): void
{
$this->accumulationStrategy = $strategy;
$this->currency = $strategy::CURRENCY;
//...
}
}
Как видите, в данной конкретной реализации стратегия расчета поощрений хранит в себе знание о том, в какой валюте она рассчитывает накопления, и именно стратегия определяет валюту для программы лояльности. Это частный случай и ваши стратегии вовсе не обязаны как-то влиять на классы, которые их в себя агрегируют.
Что дает нам такое решение:
LoyaltyProgram.LoyaltyProgram знает о том, в какой валюте хранится баланс на ее картах, но ничего не знает о том, как этот баланс рассчитывается. То есть агрегат LoyaltyProgram разгружается от части логики, делегируя ее в свои вложенные структуры.Как видите, Стратегия довольно проста и сводится к тому, чтобы вынести за общий интерфейс семейство алгоритмов (расчета поощрений, стартовой выдачи карт и т. д.)
Вообще Стратегия - один из самых простых и понятных способов создать точку расширения в вашем коде, избегая кучи if-ов и запутывания логики.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5❤1
Давно хотел поделиться своим неоднозначным опытом применения паттерна Спецификация. Вообще, все, что касается сложных выборок данных, неоднозначно. 😄
Построить аккуратный и легкочитаемый код в ситуациях, когда требуется реализовать сложные критерии отбора объектов, почти невозможно. Спецификация, по крайней мере, позволяет спустить весь треш вниз, в инфраструктурный слой.
https://habr.com/ru/articles/929848/
Построить аккуратный и легкочитаемый код в ситуациях, когда требуется реализовать сложные критерии отбора объектов, почти невозможно. Спецификация, по крайней мере, позволяет спустить весь треш вниз, в инфраструктурный слой.
https://habr.com/ru/articles/929848/
Please open Telegram to view this post
VIEW IN TELEGRAM
Хабр
Паттерн Спецификация: реальный опыт применения
Четыре года назад на собеседовании я услышал от интервьюера о том, как замечательно паттерн Спецификация помогает справиться с проблемой разрастания репозитория. Я думаю, многие с этим...
👍4🔥3
Сколько лет с этим сталкиваюсь, а все не могу привыкнуть. Ты такой весь в задаче, напряжен и собран. И тут тебе шторм выделяет русское слово красным. Ты перечитываешь его трижды и не видишь ошибки.
Раздраженный тем, что уже выронил из головы контекст задачи и переключился на орфографию, ты наводишь мышь на слово: "ну давай, научи меня русскому языку!" И видишь вот это. В лесу родилась елочка. Даже музыка заиграла в голове.
Раздраженный тем, что уже выронил из головы контекст задачи и переключился на орфографию, ты наводишь мышь на слово: "ну давай, научи меня русскому языку!" И видишь вот это. В лесу родилась елочка. Даже музыка заиграла в голове.
😁10
Про оптимистичную конкуренцию на простом примере
Недавно у нас в команде разработчик решал такую задачу:
💳 Реализовать генератор номеров пластиковых карточек. Номер состоит из двух частей: id компании и порядковый номер карты внутри компании.
1️⃣ В качестве первого решения разработчик подключил в проект Redis. Мотивация такая: редис умеет делать атомарный инкремент с отдачей нового значения, что снимает проблему конкуренции за один и тот же номер карты. То есть, исключается ситуация, когда два PHP процесса попробуют сохранить в базу карту с одинаковым номером.
☠️ Услышав о необходимости поднять на проде Redis, техлид нервно подпрыгнул и в удовольствии разработчику отказал. Оно и понятно: добавлять в систему ради такой мелкой задачи новый компонент, который будет являться еще одной потенциальной точкой отказа, не согласится ни один человек, отвечающий за стабильность прода, будь то техлид, девопс или СТО.
2️⃣ Тогда разработчик реализовал второе решение: хранение последнего порядкового номера карты
❓ Лично у меня на этом моменте возникло два вопроса:
1. Насколько удачно такое решение, и
2. а нужны ли здесь вообще блокировки?
Что касается ответа на первый вопрос, то при больших нагрузках такие блокировки превращаются в bottleneck и создают проблемы для масштабирования. Но на этом проекте нагрузки небольшие, и в пределах одной компании создается не так много пластиковых карт. И тогда мы переходим ко второму вопросу.
🤔 Когда мы рассматриваем ситуацию с конкуренцией, надо всегда помнить, что она делится на оптимистичную и пессимистичную. При оптимистичной конкуренции мы предполагаем, что конфликты редки, а значит физические блокировки не требуются.
Давайте посмотрим: у каждой компании свой
А дальше возникает вопрос: а что с этим делать? При оптимистичной конкуренции применяются семантические блокировки, но это больше для распределенных систем (микросервисы). Здесь не требуется и этого. Достаточно создать уникальный индекс в таблице пластиковых карт по полю
Следующий вопрос: а что будет, если конфликт все-таки возникнет. Ответ прост: один из PHP процессов упадет с SQL ошибкой
Если такое увидит сотрудник компании при попытке завести пластиковую карту с вероятностью в сотые или тысячные процента, то надо ли заморачиваться с блокировками? Я считаю, что нет. Чем проще ваша система и ваш код, тем легче живется как вам, так и вашим коллегам.🙂
В общем, не в том сила, чтобы чуть что, тащить в проект редис, рабита и монгу, а в том, чтобы адекватно оценивать как риски, так и стоимость разрабатываемого решения.😄
Недавно у нас в команде разработчик решал такую задачу:
💳 Реализовать генератор номеров пластиковых карточек. Номер состоит из двух частей: id компании и порядковый номер карты внутри компании.
☠️ Услышав о необходимости поднять на проде Redis, техлид нервно подпрыгнул и в удовольствии разработчику отказал. Оно и понятно: добавлять в систему ради такой мелкой задачи новый компонент, который будет являться еще одной потенциальной точкой отказа, не согласится ни один человек, отвечающий за стабильность прода, будь то техлид, девопс или СТО.
last_sequence_id в MySQL с накладыванием блокировок через SELECT FOR UPDATE. То есть, первый PHP процесс, создающий новую карту, берет из базы последний порядковый номер. И пока этот PHP процесс не завершил создание карты, сохранение ее в базу и не инкрементнул last_sequence_id, остальные процессы будут дожидаться снятия блокировки.1. Насколько удачно такое решение, и
2. а нужны ли здесь вообще блокировки?
Что касается ответа на первый вопрос, то при больших нагрузках такие блокировки превращаются в bottleneck и создают проблемы для масштабирования. Но на этом проекте нагрузки небольшие, и в пределах одной компании создается не так много пластиковых карт. И тогда мы переходим ко второму вопросу.
Давайте посмотрим: у каждой компании свой
last_sequence_id. При генерации карты он инкрементится на 1. Карта создается либо при регистрации нового клиента, либо сотрудником компании вручную. Компании имеют тысячи клиентов. Итого: вероятность ситуации, что в один и тот же момент два PHP процесса попытаются одновременно создать карту, крайне мала. Это и есть пример оптимистичной конкуренции.А дальше возникает вопрос: а что с этим делать? При оптимистичной конкуренции применяются семантические блокировки, но это больше для распределенных систем (микросервисы). Здесь не требуется и этого. Достаточно создать уникальный индекс в таблице пластиковых карт по полю
number. Это гарантирует нас от сохранения двух карт с одинаковым номером.Следующий вопрос: а что будет, если конфликт все-таки возникнет. Ответ прост: один из PHP процессов упадет с SQL ошибкой
Integrity constraint violation. И здесь вы уже сами решаете, устраивает вас это, или нет. Если ваше приложение юзер-френдли, то на фронте пользователь увидит что-то вроде "Что-то пошло не так, попробуйте еще раз."Если такое увидит сотрудник компании при попытке завести пластиковую карту с вероятностью в сотые или тысячные процента, то надо ли заморачиваться с блокировками? Я считаю, что нет. Чем проще ваша система и ваш код, тем легче живется как вам, так и вашим коллегам.
В общем, не в том сила, чтобы чуть что, тащить в проект редис, рабита и монгу, а в том, чтобы адекватно оценивать как риски, так и стоимость разрабатываемого решения.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤3🔥1
🐸 Дождь бывает не только из лягушек
Посетила меня сегодня идея сходить в бассейн попозже вечерком. Гипотеза состояла в том, что спорт-клуб пятничным вечером будет полупуст, и я смогу по такому поводу насладиться плаванием на персональной дорожке 🦊. Без толкучки, так сказать.
Поначалу все шло неплохо. Персональной дорожки мне не досталось, но народу было немного, и плавалось комфортно. Ровно до тех пор, пока не пошел... Дождь из голых мужиков!🤯
Пока я восстанавливал дыхание у бортика возле стартовой тумбы (такая штука, с которой в воду прыгают), на меня в буквальном смысле повалилась толпа голых дяденек. Спортивные, обычные, жирные и ультражирные лоснящиеся туши непрерывно обрушивались в воду, поднимая тучи брызг. Это было похоже на ковровую бомбардировку. 🛩
Кто-то прыгал с тех самых стартовых вышек прямо через мою голову, кто-то с борта, кто-то чинно спускался по лестнице. Но их было реально МНОГО. В мгновение ока идиллическая картина тихого полупустого бассейна с приятными размеренно покачивающимися волнами, превратилась в бурлящий водопой из какого-нибудь "Мадагаскара" или "Ледникового периода".🤪
Самое неприятное состояло в том, что большинство дядек, устроивших Мамаево нашествие, плевать хотели на то, что дорожки в бассейне предназначены для спортивного плавания туда-сюда, а дляостальной херни прочих активностей есть релакс-зона, не поделенная на дорожки.
Плавание из одного конца бассейна в другой мгновенно превратилось вгребанный увлекательный квест. Сначала предстояло обойти какого-то дурачка молодого парня, пытавшегося посреди дорожки выполнить что-то вроде стойки на руках, только в воде.
Дальше по курсу шли жирные, обрюзгшие мужики. Опершись на борт и разделительный канат как на барную стойку, они, перекрывая дорожку почти полностью, громко обсуждали свои похождения по бабам. Причем, послушаешь их, так у каждого не только член 30 сантиметров, но и такой список отполированных девок, что Джейсон Стэйтем сдох бы от зависти.💪
Наконец, миновав все преграды, я достиг противоположного борта, возле которого большой дядя в ярко-желтой шапочке играл желтым же корабликом 🚤 (неподалеку стоял ящик с игрушками для мамочек, приходящих с детьми в релакс-зону). Дядя то поднимал кораблик над водой, то снова бросал его на поверхность, изображая губами то ли работу двигателей то ли пердеж матросов, переевших просроченных пайков.
Остановившись возле увлеченного морской игрой "малыша" я всерьез задумался: стоит ли оставаться посреди этого театра абсурда, или пора домой. Решение помог принять мужик на соседней дорожке, решивший покинуть бассейн, не пользуясь лестницей.
По всей видимости, он рассчитывал, оперевшись руками на борт, выскочить из воды подобно мускулистому герою какого-нибудь рекламного ролика, но... Не рассчитал свои кондиции и физические возможности и, выбравшись из воды на край бассейна, был нагнут силой земного притяжения в позу рака, от чего его задница, обтянутая трусами в горошек (ну кто в бассейн такое одевает, фу), оказалась аккурат возле моего лица.
После такого чудного зрелища я решил, что лучше будет продолжить тренировку завтра, благо до клуба от дома рукой подать. Ну а единственное объяснение произошедшему я нашел такое: к вечеру позаканчивались групповые программы, всякие там файтинги, кросс-фиты и пилатесы, и народ после тренировки побрел в бассейн, дабы поймать релакс и освежиться.
Ну а сокрушаться о том, что народ у нас что на парковке, что в очереди, что в бассейне вести себя не умеет, не буду, пост и так вышел язвительным. Зато жизненным.😄
Посетила меня сегодня идея сходить в бассейн попозже вечерком. Гипотеза состояла в том, что спорт-клуб пятничным вечером будет полупуст, и я смогу по такому поводу насладиться плаванием на персональной дорожке 🦊. Без толкучки, так сказать.
Поначалу все шло неплохо. Персональной дорожки мне не досталось, но народу было немного, и плавалось комфортно. Ровно до тех пор, пока не пошел... Дождь из голых мужиков!
Пока я восстанавливал дыхание у бортика возле стартовой тумбы (такая штука, с которой в воду прыгают), на меня в буквальном смысле повалилась толпа голых дяденек. Спортивные, обычные, жирные и ультражирные лоснящиеся туши непрерывно обрушивались в воду, поднимая тучи брызг. Это было похоже на ковровую бомбардировку. 🛩
Кто-то прыгал с тех самых стартовых вышек прямо через мою голову, кто-то с борта, кто-то чинно спускался по лестнице. Но их было реально МНОГО. В мгновение ока идиллическая картина тихого полупустого бассейна с приятными размеренно покачивающимися волнами, превратилась в бурлящий водопой из какого-нибудь "Мадагаскара" или "Ледникового периода".
Самое неприятное состояло в том, что большинство дядек, устроивших Мамаево нашествие, плевать хотели на то, что дорожки в бассейне предназначены для спортивного плавания туда-сюда, а для
Плавание из одного конца бассейна в другой мгновенно превратилось в
Дальше по курсу шли жирные, обрюзгшие мужики. Опершись на борт и разделительный канат как на барную стойку, они, перекрывая дорожку почти полностью, громко обсуждали свои похождения по бабам. Причем, послушаешь их, так у каждого не только член 30 сантиметров, но и такой список отполированных девок, что Джейсон Стэйтем сдох бы от зависти.
Наконец, миновав все преграды, я достиг противоположного борта, возле которого большой дядя в ярко-желтой шапочке играл желтым же корабликом 🚤 (неподалеку стоял ящик с игрушками для мамочек, приходящих с детьми в релакс-зону). Дядя то поднимал кораблик над водой, то снова бросал его на поверхность, изображая губами то ли работу двигателей то ли пердеж матросов, переевших просроченных пайков.
Остановившись возле увлеченного морской игрой "малыша" я всерьез задумался: стоит ли оставаться посреди этого театра абсурда, или пора домой. Решение помог принять мужик на соседней дорожке, решивший покинуть бассейн, не пользуясь лестницей.
По всей видимости, он рассчитывал, оперевшись руками на борт, выскочить из воды подобно мускулистому герою какого-нибудь рекламного ролика, но... Не рассчитал свои кондиции и физические возможности и, выбравшись из воды на край бассейна, был нагнут силой земного притяжения в позу рака, от чего его задница, обтянутая трусами в горошек (ну кто в бассейн такое одевает, фу), оказалась аккурат возле моего лица.
После такого чудного зрелища я решил, что лучше будет продолжить тренировку завтра, благо до клуба от дома рукой подать. Ну а единственное объяснение произошедшему я нашел такое: к вечеру позаканчивались групповые программы, всякие там файтинги, кросс-фиты и пилатесы, и народ после тренировки побрел в бассейн, дабы поймать релакс и освежиться.
Ну а сокрушаться о том, что народ у нас что на парковке, что в очереди, что в бассейне вести себя не умеет, не буду, пост и так вышел язвительным. Зато жизненным.
Please open Telegram to view this post
VIEW IN TELEGRAM
😁14🌚2🤬1💯1