Выделение отдельного класса User для нужд Security
Почти в каждом проекте, с которым мне приходилось сталкиваться, наблюдалась одна и та же проблема: переплетение бизнес-логики и технической логики, отвечающей за аутентификацию и авторизацию.
Причиной этого является единый класс
⚙️ Аутентификация - проверка по паре логин-пароль, существует ли пользователь.
⚙️ Авторизация - хранение атрибутов, необходимых для принятия решения о доступе. Таким атрибутом очень часто выступает роль пользователя, но бывают и более тонкие и сложные системы.
⚙️ Инкапсулирование некоторой части логики контроля доступа.
⚙️ Инкапсулирование бизнес-логики.
Как правило, по мере развития проекта разрастаются обе компоненты нашего
Данная проблема решается довольно просто: разделением класса
Во второй класс мы выносим только те данные, которые необходимы для аутентификации и авторизации. Этот класс я обычно называю
При таком подходе вы разделяете ответственности и разносите доменный и инфраструктурный слои вашего приложения.
🍀 Весь код Security, принимающий решение о доступе (симфонийские
🍀 Ваш фреймворк (то есть, его конфиги) также завязывается на
🍀 В доменном
В заключение приведу укороченный пример кода
При таком подходе вы можете независимо развивать техническую инфраструктурную логику и бизнес-логику, дописывая нужный код в доменного
#Security
Почти в каждом проекте, с которым мне приходилось сталкиваться, наблюдалась одна и та же проблема: переплетение бизнес-логики и технической логики, отвечающей за аутентификацию и авторизацию.
Причиной этого является единый класс
User, который становится местом склейки доменного и инфраструктурного слоев приложения. Как правило, этот класс используется для решения сразу нескольких задач:⚙️ Аутентификация - проверка по паре логин-пароль, существует ли пользователь.
⚙️ Авторизация - хранение атрибутов, необходимых для принятия решения о доступе. Таким атрибутом очень часто выступает роль пользователя, но бывают и более тонкие и сложные системы.
⚙️ Инкапсулирование некоторой части логики контроля доступа.
⚙️ Инкапсулирование бизнес-логики.
Как правило, по мере развития проекта разрастаются обе компоненты нашего
User: инфраструктурная и доменная. Разветвляется система ролей и логика авторизации с одной стороны и развивается бизнесовая часть с другой: появляется профиль пользователя с контактами, добавляется хранение каких-нибудь предпочтений юзера, его прогресса в чем-либо или иной домен-спецэффичной информации.Данная проблема решается довольно просто: разделением класса
User на два. В первом классе остается вся бизнес-логика и все данные, включая те, которые необходимы для работы Security, такие, как логин-пароль. Причем, это не обязательно должен быть один класс. Это вполне может быть большой доменный агрегат, включающий в себя классы типа Profile, ManagerAuthorities, Preferences и т. д.Во второй класс мы выносим только те данные, которые необходимы для аутентификации и авторизации. Этот класс я обычно называю
SecurityUser. Он наполняется данными из той же таблицы, что и доменный User, но несет в себе гораздо более узкий срез этих данных. Подробнее об этом можно почитать выше в моем посте о CQRS.При таком подходе вы разделяете ответственности и разносите доменный и инфраструктурный слои вашего приложения.
Voter, иишные beforeAction, ваши собственные классы и т.д.) работают с экземпляром SecurityUser.SecurityUser, о доменном пользователе он ничего не знает.User вы развиваете бизнес-логику, не забивая себе голову тем, как это повлияет на работу Security.В заключение приведу укороченный пример кода
SecurityUser.namespace App\Infrastructure\Symfony\Security\User;
use App\Domain\Model\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
class SecurityUser implements UserInterface
{
public function __construct(
public readonly string $id
public readonly string $login
//другие атрибуты, необходимые для работы Security (роли, права и т. п.)
) {
}
public static function createFromDomainUser(User $domainUser): self
{
//...
}
//Следующие методы диктуются симфонийским UserInterface
//Согласитесь, им нечего делать в доменном классе User
public function getRoles(): array
{
}
public function eraseCredentials(): void
{
}
public function getUserIdentifier(): string
{
}
}
При таком подходе вы можете независимо развивать техническую инфраструктурную логику и бизнес-логику, дописывая нужный код в доменного
User или в SecurityUser.#Security
Please open Telegram to view this post
VIEW IN TELEGRAM
Telegram
Александр Карпов
Практическая польза CQRS
Публикую еще один кусок, который решил вырезать из своего готовящегося к публикации лонгрида.
Представьте, что нам нужно решить такую проблему: есть огромная масса legacy кода, написанного как попало, и база на 250+ таблиц, на которую…
Публикую еще один кусок, который решил вырезать из своего готовящегося к публикации лонгрида.
Представьте, что нам нужно решить такую проблему: есть огромная масса legacy кода, написанного как попало, и база на 250+ таблиц, на которую…
🔥1
Почему фабрика - самый превратно понимаемый паттерн после стратегии
Ну во-первых потому, что такого паттерна, строго говоря, вообще нет. Есть
Во-первых, мы можем развивать линейку порождаемых объектов. В терминологии паттерна эти объекты называются продуктами. В качестве примера первоисточник приводит элементы интерфейса: окно, скролл-бар, кнопка и т. д.
Соответственно, мы можем независимо развивать дерево каждого из этих продуктов: писать классы-наследники базовой кнопки, наследники базового окна и так далее.
Второе, что мы можем развивать независимо - это дерево классов-фабрик, порождающих эти объекты. То есть, первая фабрика умеет порождать первый вид окон, кнопок и скролл-баров, вторая фабрика умеет порождать второй вид, и т. д.
Поскольку все фабрики и порождаемые ими продукты имеют наследуемый интерфейс, мы получаем возможность полиморфно подменять конкректные реализации фабрик, а значит и генерируемых ими продуктов, прямо в рантайме, меняя тем самым поведение приложения.
Вообще, полиморфизм - это ключ к понимаю всех паттернов. Я считаю, что именно непонимание принципа полиморфизма приводит к искажению (я бы даже сказал, извращению) понимания паттернов. Я уже писал пост про инкапсуляцию. Надо будет в будущем сделать пост и про полиморфизм.
Так вот: за полчаса я не смог вспомнить ни один пример использования "Абстрактной фабрики" в своей восьмилетней практике. Уж больно глобальная штука.
Например, у вас есть потребность генерировать разные представления некоторого объекта (скорее всего, это будет сложный агрегат). Соответственно, вы можете объявить абстрактные классы представления
Дальше вы пишете код, который зависит от этих классов. Этот код знает, что ему нужен некий форматтер, который породит некоторое представление, которое будет передано дальше: отправлено в качестве ответа на http запрос или послано в шину сообщений.
Меняя форматтер вы меняете и представление. При этом конкретные реализации форматтеров могут быть довольно толстыми, если им требуется обогащать представление объекта данными из разных источников.
На самом деле, хоть "Фабричный метод" и попроще "Абстрактной фабрики", случаи его применения не так уж и часты.
Продолжение в следующем посте.⤵️
Ну во-первых потому, что такого паттерна, строго говоря, вообще нет. Есть
Абстрактная фабрика и Фабричный метод.Абстрактная фабрика - довольно монструозная конструкция, позволяющая параллельно развивать несколько деревьев классов.Во-первых, мы можем развивать линейку порождаемых объектов. В терминологии паттерна эти объекты называются продуктами. В качестве примера первоисточник приводит элементы интерфейса: окно, скролл-бар, кнопка и т. д.
Соответственно, мы можем независимо развивать дерево каждого из этих продуктов: писать классы-наследники базовой кнопки, наследники базового окна и так далее.
Второе, что мы можем развивать независимо - это дерево классов-фабрик, порождающих эти объекты. То есть, первая фабрика умеет порождать первый вид окон, кнопок и скролл-баров, вторая фабрика умеет порождать второй вид, и т. д.
Поскольку все фабрики и порождаемые ими продукты имеют наследуемый интерфейс, мы получаем возможность полиморфно подменять конкректные реализации фабрик, а значит и генерируемых ими продуктов, прямо в рантайме, меняя тем самым поведение приложения.
Вообще, полиморфизм - это ключ к понимаю всех паттернов. Я считаю, что именно непонимание принципа полиморфизма приводит к искажению (я бы даже сказал, извращению) понимания паттернов. Я уже писал пост про инкапсуляцию. Надо будет в будущем сделать пост и про полиморфизм.
Так вот: за полчаса я не смог вспомнить ни один пример использования "Абстрактной фабрики" в своей восьмилетней практике. Уж больно глобальная штука.
Фабричный метод - более простая конструкция, предполагающая развитие двух деревьев классов - продукта (порождаемого объекта) и создателя (того, кто порождает). Опять же, здесь (как и в любом паттерне) важен полиморфизм: подменяя создателя, вы подменяете и то, что он создает.Например, у вас есть потребность генерировать разные представления некоторого объекта (скорее всего, это будет сложный агрегат). Соответственно, вы можете объявить абстрактные классы представления
AbstractRepresentation и форматтера AbstractFormatter.Дальше вы пишете код, который зависит от этих классов. Этот код знает, что ему нужен некий форматтер, который породит некоторое представление, которое будет передано дальше: отправлено в качестве ответа на http запрос или послано в шину сообщений.
Меняя форматтер вы меняете и представление. При этом конкретные реализации форматтеров могут быть довольно толстыми, если им требуется обогащать представление объекта данными из разных источников.
На самом деле, хоть "Фабричный метод" и попроще "Абстрактной фабрики", случаи его применения не так уж и часты.
Продолжение в следующем посте.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Продолжаем разговор про фабрики ⬆️
Так вот: если оба паттерна применяются редко, то что же тогда люди понимают под "Фабрикой", ведь мы слышим это слово постоянно? По моим ощущениям от общения, под "Фабрикой" понимается вообще любой порождающий код🧠 . В лучшем случае, вот такой:
Такой код, разумеется, имеет право на жизнь, но только он не реализует ни один из двух паттернов. Для "Фабричного метода" ему не хватает единого создателя. Например,
От "Абстрактной фабрики" в примере выше нет вообще ничего. Фабрика у нас конкретная и одна (то есть, расширение дерева фабрик не предполагается), а вместо семейства связанных объектов порождаются подвиды одного объекта - менеджера.
Вот если бы один и тот же класс имел методы
В худшем же случае фабрикой называют вообще любой код, дергающий конструктор и возвращающий его результат.
Где мешает такое непонимание этих паттернов? Мне, например, это дико мешает на собеседованиях. Мне встречалось немало интервьюеров, просящих во время лайв-кодинга "сделать фабрику", или говорящих "ты не сделал фабрику". Дальше начиналась дискуссия на тему "а зачем тебе фабрика", в ходе которой выяснялось, что чувак просто хотел увидеть вынос вызова конструктора в отдельный класс.
Зачем это нужно в конкретном примере, объяснить такие знатоки не могут. Таким образом они "проверяют твое мышление и знание паттернов"🤪 . Я думаю, если вы дочитали оба поста про фабрики до конца, то вы наверняка понимаете, почему от таких интервьюеров мне каждый раз становится очень грустно. 😢
Особенно если фраза "не сделал фабрику" фигурирует в причине отказа сделать оффер по итогу техинтервью. Впрочем, о том, что таким отказам стоит только радоваться, я уже писал.
#Паттерны, #Собеседования
Так вот: если оба паттерна применяются редко, то что же тогда люди понимают под "Фабрикой", ведь мы слышим это слово постоянно? По моим ощущениям от общения, под "Фабрикой" понимается вообще любой порождающий код
abstract class Manager {}
class SupportManager extends Manager {}
class SalesManager extends Manager {}
class OfficeManager extends Manager {}
class ManagerFactory {
public function createSupport(): SupportManager
{
//...
}
public function createSales(): SalesManager
{
//...
}
public function createOffice(): OfficeManager
{
//...
}
}
Такой код, разумеется, имеет право на жизнь, но только он не реализует ни один из двух паттернов. Для "Фабричного метода" ему не хватает единого создателя. Например,
AbstractDepartment с методом createManager(): Manager. Подменяя департамент, мы подменяем менеджера, который из него, пардон, исторгается.От "Абстрактной фабрики" в примере выше нет вообще ничего. Фабрика у нас конкретная и одна (то есть, расширение дерева фабрик не предполагается), а вместо семейства связанных объектов порождаются подвиды одного объекта - менеджера.
Вот если бы один и тот же класс имел методы
createManager(), createMarketer() и createAccountant() для порождения менеджера, маркетолога и бухгалтера какого-то конкретного вида, то это была бы "Абстрактная фабрика".В худшем же случае фабрикой называют вообще любой код, дергающий конструктор и возвращающий его результат.
Где мешает такое непонимание этих паттернов? Мне, например, это дико мешает на собеседованиях. Мне встречалось немало интервьюеров, просящих во время лайв-кодинга "сделать фабрику", или говорящих "ты не сделал фабрику". Дальше начиналась дискуссия на тему "а зачем тебе фабрика", в ходе которой выяснялось, что чувак просто хотел увидеть вынос вызова конструктора в отдельный класс.
Зачем это нужно в конкретном примере, объяснить такие знатоки не могут. Таким образом они "проверяют твое мышление и знание паттернов"
Особенно если фраза "не сделал фабрику" фигурирует в причине отказа сделать оффер по итогу техинтервью. Впрочем, о том, что таким отказам стоит только радоваться, я уже писал.
#Паттерны, #Собеседования
Please open Telegram to view this post
VIEW IN TELEGRAM
Telegram
Александр Карпов
Стоит ли расстраиваться, получив отказ после техинтервью? Я считаю, нужно радоваться. Давайте разберемся, какие могут быть причины отказа.
1. Специалист, проводивший техинтервью (сеньор, тимлид, техлид, начальник разработки, СТО) решил, что ваших навыков…
1. Специалист, проводивший техинтервью (сеньор, тимлид, техлид, начальник разработки, СТО) решил, что ваших навыков…
👏2👍1
Вот и готов к публикации мой второй лонгрид. На сей раз о том, как JWT помогает сделать архитектуру нашего приложения более изящной и правильной. Рассмотрены примеры из моей практики для микросервисов и монолитов.
🪶 Публикация на Хабре
🪶 Публикация на Substack
🪶 Публикация на vc.ru
Ссылка на примеры кода для этой статьи: https://github.com/slayervc/jwt_example
#Лонгриды
🪶 Публикация на Хабре
🪶 Публикация на Substack
🪶 Публикация на vc.ru
Ссылка на примеры кода для этой статьи: https://github.com/slayervc/jwt_example
#Лонгриды
👏2
На какую тему вам было бы интереснее прочитать следующий лонгрид? Можно выбрать несколько вариантов.
Final Results
42%
Разбор кейса с выносом из двух монолитов единого сервиса паспорта
32%
Гайд по использованию профайлера xdebug с практическими примерами
63%
Разбор проекта, построенного на паттернах Композит, Визитер, Декоратор и Цепочка обязанностей
53%
Паттерн Спецификация
Катание на ватрушках – один из вариантов зимнего активного отдыха, который не требует подготовки, навыков или снаряжения. Достаточно одеться потеплее и пнуть себя под зад.
Чтобы активный отдых стал совсем активным, можно, расправив плечи, гордо плюнуть на подъемник и самостоятельно затащить в гору свою ватрушку с сидящем на ней ребетенком.💪
Трасса может быть похардкорнее (для молодых и/или холостых) или полайтовее (для тех, кто с детьми). Поскольку мы из второй категории, то от посещения трассы, которую ругают за безопасность, отказались и поехали в гостиничный комплекс возле Байкала: трасса там прямая, а за каждым спуском следит инструктор.
В общем, в качестве прививки от вылезания глаз на лоб по случаю бесконечного сидения перед монитором очень даже годится.
#отдых_от_работы
Чтобы активный отдых стал совсем активным, можно, расправив плечи, гордо плюнуть на подъемник и самостоятельно затащить в гору свою ватрушку с сидящем на ней ребетенком.
Трасса может быть похардкорнее (для молодых и/или холостых) или полайтовее (для тех, кто с детьми). Поскольку мы из второй категории, то от посещения трассы, которую ругают за безопасность, отказались и поехали в гостиничный комплекс возле Байкала: трасса там прямая, а за каждым спуском следит инструктор.
В общем, в качестве прививки от вылезания глаз на лоб по случаю бесконечного сидения перед монитором очень даже годится.
#отдых_от_работы
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Как понять полиморфизм
Когда я объясняю товарищам по команде полиморфизм, то обычно делаю это через аналогию, обращаясь к мультфильму "Город героев". К слову, мультфильм рекомендую как к семейному просмотру с детьми, так и к холостяцкому под свежее пивко.
Для тех, кто не смотрел, перескажу то, что важно для понимания полиморфизма. В мультфильме есть человекоподобный медицинский робот, созданный заботиться о здоровье и выражать сочувствие пациентам. На корпусе этого робота есть слот, куда вставлен диск (зелененький такой) с медицинскими программами, определяющими поведение робота.
По ходу развития сюжета главный герой создает новый диск (красненький), на котором записана программа для применения роботом приемов рукопашной. Что произошло в тот момент, когда зеленый диск был вытащен из робота и заменен на красный, догадаться несложно: робот перестал проявлять заботу об окружающих и начал раздавать негодяям леща (пардон за скучный лексикон, но в выборе фраз ваш автор стеснен, т. к. с удивлением обнаружил недавно, что его канал читают дети).
Так вот. Давайте проведем аналогии с кодом.
Робот - это метод класса. Слот для дисков на теле робота - это аргумент вашего метода, а конкретный инстанс, который вы передаете в метод, когда его вызываете - это диск, определяющий поведение робота. Передали зеленый диск - лечим. Передали красный диск - калечим. Можно придумать желтый диск, и робот начнет работать клоуном, кривляясь перед прохожими.
Без полиморфизма наследование превращается в унылую альтернативу копипасты. Когда мы объявляем интерфейс (под этим словом надо понимать не только конструкцию
Класс, реализующий интерфейс, обязан принимать запросы на выполнение операций, описанных в интерфейсе. Что он, класс, будет делать дальше, зависит от реализации: лечить, калечить, кривляться, стоять неподвижно и писать полученные запросы в лог, делегировать выполнение запросов другим объектам и т. д.
Именно возможность создавать бесконечное количество реализаций любого интерфейса (кроме финальных классов, будь они неладны) и, самое главное, подменять эти реализации вплоть до рантайма и называется полиморфизмом.
Именно на принципе полиморфизма построены все (или почти все) паттерны "Банды четырех". Взять хотя бы набивший оскомину паттерн "Стратегия". Собственно, в аналогии выше он и применен: реализуя разные классы стратегий мы задаем поведение робота.
Вот почему важен принцип Лисков: нарушая его, вы ограничиваете возможности по использованию полиморфизма. Как я писал выше, думая об инкапсуляции, думайте об интерфейсе. А думая об интерфейсе, помните о полиморфизме.
То есть, задумывайтесь о том, достаточно ли свободы дает ваш интерфейс для написания различных реализаций, меняющих поведение вашего приложения. В том числе таким образом, который сегодня вам не нужен, но может понадобиться завтра.
#ООП #полиморфизм #интерфейс_класса #Liskov
Когда я объясняю товарищам по команде полиморфизм, то обычно делаю это через аналогию, обращаясь к мультфильму "Город героев". К слову, мультфильм рекомендую как к семейному просмотру с детьми, так и к холостяцкому под свежее пивко.
Для тех, кто не смотрел, перескажу то, что важно для понимания полиморфизма. В мультфильме есть человекоподобный медицинский робот, созданный заботиться о здоровье и выражать сочувствие пациентам. На корпусе этого робота есть слот, куда вставлен диск (зелененький такой) с медицинскими программами, определяющими поведение робота.
По ходу развития сюжета главный герой создает новый диск (красненький), на котором записана программа для применения роботом приемов рукопашной. Что произошло в тот момент, когда зеленый диск был вытащен из робота и заменен на красный, догадаться несложно: робот перестал проявлять заботу об окружающих и начал раздавать негодяям леща (пардон за скучный лексикон, но в выборе фраз ваш автор стеснен, т. к. с удивлением обнаружил недавно, что его канал читают дети).
Так вот. Давайте проведем аналогии с кодом.
public function robot(BehaviorInterface $behavior)
Робот - это метод класса. Слот для дисков на теле робота - это аргумент вашего метода, а конкретный инстанс, который вы передаете в метод, когда его вызываете - это диск, определяющий поведение робота. Передали зеленый диск - лечим. Передали красный диск - калечим. Можно придумать желтый диск, и робот начнет работать клоуном, кривляясь перед прохожими.
Без полиморфизма наследование превращается в унылую альтернативу копипасты. Когда мы объявляем интерфейс (под этим словом надо понимать не только конструкцию
Interface, но и любой класс), мы объявляем прежде всего набор операций и говорим, что класс, реализующий этот интерфейс обязан... Нет, не выполнять эти операции.Класс, реализующий интерфейс, обязан принимать запросы на выполнение операций, описанных в интерфейсе. Что он, класс, будет делать дальше, зависит от реализации: лечить, калечить, кривляться, стоять неподвижно и писать полученные запросы в лог, делегировать выполнение запросов другим объектам и т. д.
Именно возможность создавать бесконечное количество реализаций любого интерфейса (кроме финальных классов, будь они неладны) и, самое главное, подменять эти реализации вплоть до рантайма и называется полиморфизмом.
Именно на принципе полиморфизма построены все (или почти все) паттерны "Банды четырех". Взять хотя бы набивший оскомину паттерн "Стратегия". Собственно, в аналогии выше он и применен: реализуя разные классы стратегий мы задаем поведение робота.
Вот почему важен принцип Лисков: нарушая его, вы ограничиваете возможности по использованию полиморфизма. Как я писал выше, думая об инкапсуляции, думайте об интерфейсе. А думая об интерфейсе, помните о полиморфизме.
То есть, задумывайтесь о том, достаточно ли свободы дает ваш интерфейс для написания различных реализаций, меняющих поведение вашего приложения. В том числе таким образом, который сегодня вам не нужен, но может понадобиться завтра.
#ООП #полиморфизм #интерфейс_класса #Liskov
Кинопоиск
«Город героев» (Big Hero 6, 2014)
🎬 Юный Хиро Хамада — прирожденный изобретатель и гений конструирования роботов. Вместе со старшим братом Тадаши они воплощают в жизнь самые передовые идеи в Техническом университете города будущего Сан-Франсокио. После серии загадочных событий друзья оказываются…
👍4👏1🌚1
Хеш-таблицы против массивов
Как известно, время чтения из хеш-таблицы =
Первая пара методов запоминает id, сохраняя его в хеш-таблицу (первый метод) и в массив (второй метод). Вторая пара методов удаляет id из хеш-таблицы (первый метод) и из массива (второй метод).
Какой код приятнее, судите сами. Что касается стоимости обоих вариантов, то я набросал вот такой код:
Результаты его выполнения вы можете видеть в таблице, прикрепленной к посту:
☘️ Если в массиве не выполнять проверку на дубликаты, то скорость записи в обе структуры данных практически одинакова.
☘️ Проверка на дубликаты в массиве при записи в него сразу делает эту структуру данных несопоставимо медленнее.
☘️ Профит от скорости хеш-таблицы возрастает с ростом объема данных, что происходит всегда при противопоставлении алгоритмов со временем выполнения
☘️ Хеш-таблица требует существенно больше памяти. В моем случае в 1,7 - 2,5 раза, смотря чем измерять (измерял еще профайлером xdebug).
Выводы здесь довольно простые.
1️⃣ На малых объемах данных (десятки записей) хеш-таблица просто удобнее с точки зрения написания кода.
2️⃣ На больших объемах добавляется существенное преимущество в скорости за счет потребления большего количества памяти.
Если вам вдруг нужно сохранять хеш-таблицу в хранилище, то можно просто сделать
#performance
Как известно, время чтения из хеш-таблицы =
O(1), а из массива (при бинарном поиске) = O(logn). Но, помимо того, что хеш-таблица быстрее при чтении, она еще и удобнее в коде. Давайте рассмотрим типичную задачу: нужно хранить набор guid сущностей. Вот код:public function hashMapWrite(string $id): void
{
$this->hashMap[$id] = true;
}
public function arrayWrite(string $id): void
{
//Исключаем дубликаты
if (in_array($id, $this->array)) {
return;
}
$this->array[] = $id;
}
public function hashMapReadAndUnset(string $id): void
{
unset($this->hashMap[$id]);
}
public function arrayReadAndUnset(string $id): void
{
$key = array_search($id, $this->array);
unset($this->array[$key]);
}
Первая пара методов запоминает id, сохраняя его в хеш-таблицу (первый метод) и в массив (второй метод). Вторая пара методов удаляет id из хеш-таблицы (первый метод) и из массива (второй метод).
Какой код приятнее, судите сами. Что касается стоимости обоих вариантов, то я набросал вот такой код:
$ids = [];
$count = 1000;
for ($i=1; $i<= $count; $i++) {
$ids[] = $this->idGenerator->generate();
}
$start = microtime(true);
$mStart = memory_get_usage();
foreach ($ids as $id) {
$o->hashMapWrite($id);
}
$mTotal = (memory_get_usage() - $mStart) / 1024;
$execution = microtime(true) - $start;
$start = microtime(true);
$mStart = memory_get_usage();
foreach ($ids as $id) {
$o->arrayWrite($id);
}
$mTotal = (memory_get_usage() - $mStart) / 1024;
$execution = microtime(true) - $start;
shuffle($ids);
$start = microtime(true);
foreach ($ids as $id) {
$o->hashMapReadAndUnset($id);
}
$execution = microtime(true) - $start;
$start = microtime(true);
foreach ($ids as $id) {
$o->arrayReadAndUnset($id);
}
$execution = microtime(true) - $start;
Результаты его выполнения вы можете видеть в таблице, прикрепленной к посту:
☘️ Если в массиве не выполнять проверку на дубликаты, то скорость записи в обе структуры данных практически одинакова.
☘️ Проверка на дубликаты в массиве при записи в него сразу делает эту структуру данных несопоставимо медленнее.
☘️ Профит от скорости хеш-таблицы возрастает с ростом объема данных, что происходит всегда при противопоставлении алгоритмов со временем выполнения
O(1) и O(logn).☘️ Хеш-таблица требует существенно больше памяти. В моем случае в 1,7 - 2,5 раза, смотря чем измерять (измерял еще профайлером xdebug).
Выводы здесь довольно простые.
Если вам вдруг нужно сохранять хеш-таблицу в хранилище, то можно просто сделать
array_keys(). Если пользуетесь доктриной, то можно написать вот такой кастомный тип:class HashMapType extends Type
{
public function getName(): string
{
return 'hash_map';
}
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return 'JSONB';
}
public function convertToPHPValue($value, AbstractPlatform $platform): mixed
{
$keys = json_decode($value, true);
if (empty($keys)) {
return $keys;
}
$values = array_fill(0, count($keys), true);
return array_combine($keys, $values);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform): mixed
{
return json_encode(array_keys($value));
}
}
#performance
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Так ли страшна ручная гидрация?
Когда коробочный функционал ORM перестает удовлетворять нашим нуждам, появляется ручная гидрация. Как правило, это случается в следующих случаях:
⚙️ Для получения данных используется сложный SQL или DQL запрос.
⚙️ Мы хотим четко разделить доменный и инфраструктурный слои в своем проекте.
С первым случаем, я думаю, все понятно и возникает он, как правило, не слишком часто. А когда возник, то деваться нам уже некуда и - хочешь, не хочешь - ручную гидрацию выполнять придется.
Я бы хотел остановиться на втором случае, т. к. здесь речь идет о добровольном решении, фундаментально влияющем на архитектуру нашего кода. Если совсем кратко, то использование доктрины так, как это советует симфонийская дока, не очень правильно (точнее, совсем неправильно) с точки зрения слоистой архитектуры, DDD и даже SOLID.
👎 Разметка доменных сущностей доктриновскими аннотациями - это смешение данных и метаданных. Особенно некрасиво в этом ключе смотрятся аннотации, указывающие, какие следует построить индексы в таблице, или вот такая прелесть:
👎 Доменный слой ничего не должен знать ни о каких других слоях, а при описываемом подходе наши доменные сущности становятся зависимыми от инфраструктурных компонентов.
👎 Вишенкой на торте здесь выступают доктриновские коллекции, лишая нас возможности нормально писать собственную логику для своих коллекций.
Как человек, уже три года как отказавшийся от использования Доктрины "по симфонийской доке", могу уверенно сказать:
Для того, чтобы отделить метаданные от данных, разделив доменный и инфраструктурный слой, придется перейти на XML mapping. По началу это может казаться дичью, но это только по началу. Со временем к такому подходу привыкаешь, и он становится намного приятнее: все данные о том, как хранятся наши сущности, собраны в одном месте и легко читаются.
Единственной, по сути, ценой, которую приходится платить за такой подход, является ручная гидрация. Это не значит, что ее придется выполнять для каждой сущности. Вот пример репозитория для простой сущности, не являющейся агрегатом:
Как видите, никакой ручной гидрации здесь вообще нет. Зато доменный
Однако для тяжелых доменных агрегатов реализация их чтения и записи может быть действительно трудозатратной. Но об этом я расскажу уже в завтрашнем посте. Что поделать, формат tg-поста, он такой.☹️
#гидрация #Doctrine
Когда коробочный функционал ORM перестает удовлетворять нашим нуждам, появляется ручная гидрация. Как правило, это случается в следующих случаях:
⚙️ Для получения данных используется сложный SQL или DQL запрос.
⚙️ Мы хотим четко разделить доменный и инфраструктурный слои в своем проекте.
С первым случаем, я думаю, все понятно и возникает он, как правило, не слишком часто. А когда возник, то деваться нам уже некуда и - хочешь, не хочешь - ручную гидрацию выполнять придется.
Я бы хотел остановиться на втором случае, т. к. здесь речь идет о добровольном решении, фундаментально влияющем на архитектуру нашего кода. Если совсем кратко, то использование доктрины так, как это советует симфонийская дока, не очень правильно (точнее, совсем неправильно) с точки зрения слоистой архитектуры, DDD и даже SOLID.
columnDefinition: 'JSONB NOT NULL'.Как человек, уже три года как отказавшийся от использования Доктрины "по симфонийской доке", могу уверенно сказать:
Разработка в такой парадигме, в которой Доктрина "знает свое место" гораздо приятнее, особенно на длинных дистанциях, когда проект развивается.
Для того, чтобы отделить метаданные от данных, разделив доменный и инфраструктурный слой, придется перейти на XML mapping. По началу это может казаться дичью, но это только по началу. Со временем к такому подходу привыкаешь, и он становится намного приятнее: все данные о том, как хранятся наши сущности, собраны в одном месте и легко читаются.
Единственной, по сути, ценой, которую приходится платить за такой подход, является ручная гидрация. Это не значит, что ее придется выполнять для каждой сущности. Вот пример репозитория для простой сущности, не являющейся агрегатом:
namespace App\Infrastructure\PortAdapter\Repository\User;
use Doctrine\ORM\EntityRepository;
use App\Domain\Model\User\User;
use App\Domain\Model\User\UserRepositoryInterface;
class DoctrineUserRepository implements UserRepositoryInterface
{
private EntityRepository $userRepository;
public function __construct(EntityManagerInterface $em)
{
$this->userRepository = $em->getRepository(User::class);
}
public function save(User $user): void
{
$this->em->persist($user);
$this->em->flush();
}
public function findById(string $id): ?User
{
return $this->userRepository->find($id);
}
}
Как видите, никакой ручной гидрации здесь вообще нет. Зато доменный
User ничего не знает о том, как он хранится, а DoctrineUserRepository является лишь одной из возможных реализаций UserRepositoryInterface. Понадобится, напишем реализацию для монги или внешнего микросервиса.Однако для тяжелых доменных агрегатов реализация их чтения и записи может быть действительно трудозатратной. Но об этом я расскажу уже в завтрашнем посте. Что поделать, формат tg-поста, он такой.
#гидрация #Doctrine
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Продолжаем разговор про ручную гидрацию
Во вчерашнем посте⬆️ я писал, что ручная гидрация при записи и чтении громоздкого агрегата может быть трудоемкой. Конкретно нам придется сталкиваться вот с такими проблемами:
⚙️ клонирование вложенных объектов при их изменении;
⚙️ гидрация коллекций, сохраняемых как массив id;
⚙️ решение проблем с суррогатным ID для объектов-значений;
Поскольку формат телеграм-поста не позволяет рассмотреть все проблемы за раз, буду рассматривать их по одной в день.😄
#гидрация
Во вчерашнем посте
⚙️ клонирование вложенных объектов при их изменении;
⚙️ гидрация коллекций, сохраняемых как массив id;
⚙️ решение проблем с суррогатным ID для объектов-значений;
Поскольку формат телеграм-поста не позволяет рассмотреть все проблемы за раз, буду рассматривать их по одной в день.
#гидрация
Please open Telegram to view this post
VIEW IN TELEGRAM
Как заставить Доктрину отслеживать изменения вложенных объектов?
Когда мы отказываемся от доктриновских коллекций и переходим на собственные, доктрина перестает видеть изменения, происходящие в коллекциях. Давайте рассмотрим вот такой код:
Для класса
Далее где-то по коду мы делаем такую вещь:
Если
Дело в том, что внутри этого метода проверка на то, изменилось ли значение свойства, выглядит так:
Для объектов такое сравнение будет давать
Чтобы решить проблему, нужно клонировать значение свойства
В примере выше при изменении состава тегов в
Далее, мы в репозитории проверяем, изменялись ли теги для сохраняемого
Про методы
К слову, все описанное выше относится не только к коллекциям, но и к ситуациям, когда вы решили сохранять вложенный объект в виде json. Например, в профиле пользователя у вас есть поле
На этом все, завтра расскажу о том, как доставать из базы коллекцию объектов сохраненную как простой массив id.
#гидрация #Doctrine
Когда мы отказываемся от доктриновских коллекций и переходим на собственные, доктрина перестает видеть изменения, происходящие в коллекциях. Давайте рассмотрим вот такой код:
class Membership
{
private TagCollection $tags;
public function addTag(Tag $tag){
$this->tags->add($tag);
}
}
class TagCollection
{
/** @var Tag[] */
private array $elements;
public function __construct(Tag ...$tags)
{
$this->elements = $tags
}
public function add(Tag $tag): void
//...
}
Для класса
TagCollection у нас написан кастомный доктриновский тип (о нем я напишу в следующем посте), который сохраняет все теги в один столбец, конвертируя коллекцию тегов в json.Далее где-то по коду мы делаем такую вещь:
$membership->addTag(new Tag('foo'));
$this->em->persist($membership);
$this->em->flush();Если
$membership - это новая сущность, созданная через конструктор, то все пройдет гладко. Но, если $membership был извлечен из репозитория, то изменения в свойстве $tags не сохранятся. Проблема кроется в методе Doctrine\ORM\UnitOfWork::computeChangeSet(), который отвечает за отслеживание того, какие свойства каких сущностей изменились, чтобы в будущем по этим changeSets отправить в базу запросы только для изменившихся сущностей, а не для всех подряд.Дело в том, что внутри этого метода проверка на то, изменилось ли значение свойства, выглядит так:
// skip if value haven't changed
if ($orgValue === $actualValue) {
continue;
}
Для объектов такое сравнение будет давать
false только в случае, когда операнды являются разными инстансами. А поскольку на протяжении жизни объекта $membership его свойство $tags ссылается на один и тот же инстанс TagCollection, то доктрина никогда не узнает, что внутри TagCollection произошли какие-то изменения.Чтобы решить проблему, нужно клонировать значение свойства
$tags нашего $membership при его сохранении. Это можно делать либо безусловно, либо только в тех случаях, когда внутри $tags произошли какие-то изменения. Для реализации второго варианта вам придется самостоятельно отслеживать эти изменения.class DoctrineMembershipRepository
{
public function save(Membership $membership): void
{
$tagsChanged = DoctrineEntityChangesObserver::instance()->isMembershipChanged($membership->getId());
if ($tagsChanged) {
$tags = $this->getPropertyValue($membership, 'tags');
$this->setPropertyValue($membership, 'tags', clone $tags);
}
$this->em->persist($membership);
$this->em->flush();
}
}
В примере выше при изменении состава тегов в
Membership выбрасывается доменное событие, обработчик которого сохраняет id измененного Membership, используя класс DoctrineEntityChangesObserver (его код я опускаю, чтобы не загромождать пост).Далее, мы в репозитории проверяем, изменялись ли теги для сохраняемого
Membership, и клонируем при необходимости значение поля $tags. Повторюсь: можно не заморачиваться с реализацией DoctrineEntityChangesObserver и просто клонировать значение $tags при каждом сохранении.Про методы
setPropertyValue() и getPropertyValue() можно посмотреть вот в этом посте.К слову, все описанное выше относится не только к коллекциям, но и к ситуациям, когда вы решили сохранять вложенный объект в виде json. Например, в профиле пользователя у вас есть поле
contacts, представленное отдельным классом с полями email, phone и т. д.На этом все, завтра расскажу о том, как доставать из базы коллекцию объектов сохраненную как простой массив id.
#гидрация #Doctrine
👍5
Гидрация коллекций, сохраняемых как массив id
Еще одна проблема, с которой нам придется столкнуться при отказе от использования Доктрины "по симфонийской доке", это гидрация коллекций. Рассмотрим ее все на том же примере.
В примере выше класс
Для этого нам потребуется кастомный доктриновский тип. Когда в этом типе мы будем реализовывать сохранение в базу, у нас не возникнет проблем:
А вот при чтении из базы возникает проблема. Из базы мы получили массив id тегов, из которого нам нужно сделать полноценный
Мне приходилось видеть "нехорошие" варианты решения этой задачи, такие как добавление в доктриновский тип состояния:
Второй ужасный вариант - это попытаться прямо из доктриновского типа дернуть теги из базы. Эдакий иишный
Если все-таки задаться поиском архитектурно приемлемого решения, то что можно сделать? Да по сути ровно то, для чего наш класс кастомного доктриновского типа предназначен и на что он способен: вернуть недогидрированную коллекцию.
А вот код самой коллекции:
При таком решении доктриновский EntityManager будет доставать нам из базы недогидрированную сущность. Но то, что так будет, мы должны были понимать в момент принятия решения о сохранении поля
После того, как мы получим от доктрины наш недогидрированный агрегат, нам останется только запросить теги из базы по
#гидрация #Doctrine
Еще одна проблема, с которой нам придется столкнуться при отказе от использования Доктрины "по симфонийской доке", это гидрация коллекций. Рассмотрим ее все на том же примере.
class Membership
{
private TagCollection $tags;
public function addTag(Tag $tag){
$this->tags->add($tag);
}
}
class TagCollection
{
/** @var Tag[] */
private array $elements;
public function __construct(Tag ...$tags)
{
$this->elements = $tags
}
public function add(Tag $tag): void
//...
}
В примере выше класс
Tag является сущностью со своим guid и набором полей. Класс Membership связан с Tag релейшеном many-to-many. Предположим, мы решили не делать таблицу tags_memberships для реализации связи. Вместо этого мы захотели хранить в таблице memberships столбец tags, где в виде json-массива будут перечислены guid тегов, которые навешаны на каждый membersip.Для этого нам потребуется кастомный доктриновский тип. Когда в этом типе мы будем реализовывать сохранение в базу, у нас не возникнет проблем:
use Doctrine\DBAL\Types\Type;
class TagCollectionType extends Type
{
public function convertToDatabaseValue($value, AbstractPlatform $platform): mixed
{
/** @var TagCollection $value */
$array = array_map(fn(Tag $tag) => $tag->getId(), $value->getElements());
return json_encode($array);
}
}
А вот при чтении из базы возникает проблема. Из базы мы получили массив id тегов, из которого нам нужно сделать полноценный
TagCollection, содержащий объекты класса Tag со всеми остальными (помимо id) полями. И сделать нам это нужно изнутри доктриновского типа:public function convertToPHPValue($value, AbstractPlatform $platform): mixed
{
$ids = json_decode($value, true);
$tags = [];
//Как нам здесь получить теги?
return new TagCollection(...$tags);
}
Мне приходилось видеть "нехорошие" варианты решения этой задачи, такие как добавление в доктриновский тип состояния:
class TagCollectionType extends Type
{
public static array $tags = [];
}
class DoctrineMembershipRepository
{
public function findById(string $id): ?Membership
{
//сначала отдельно запросим из базы $tagIds
TagCollectionType::$tags = $this->tagRepository->findBy($tagIds);
$membership = $this->membershipRepository->find($id);
}
}
Второй ужасный вариант - это попытаться прямо из доктриновского типа дернуть теги из базы. Эдакий иишный
ActiveRecord, доступный глобально.Если все-таки задаться поиском архитектурно приемлемого решения, то что можно сделать? Да по сути ровно то, для чего наш класс кастомного доктриновского типа предназначен и на что он способен: вернуть недогидрированную коллекцию.
public function convertToPHPValue($value, AbstractPlatform $platform): mixed
{
$ids = empty($value) ? [] : json_decode($value, true);
return new NonHydratedTagCollection(...$ids);
}
А вот код самой коллекции:
class NonHydratedTagCollection extends TagCollection
{
private array $ids;
public function __construct(string ...$ids)
{
$this->ids = $ids;
}
public function hydrate($membershipId, Tag ...$tags): TagCollection
{
$tagIds = array_map(fn(Tag $tag) => $tag->getId(), $tags);
if (count($this->ids) !== count($tagIds) || !empty(array_diff($this->ids, $tagIds))) {
throw new HydrationException('The provided tags do not match the stored IDs.');
}
return new TagCollection($membershipId, ...$tags);
}
public function getIds(): array
{
return $this->ids;
}
}
При таком решении доктриновский EntityManager будет доставать нам из базы недогидрированную сущность. Но то, что так будет, мы должны были понимать в момент принятия решения о сохранении поля
$tags класса Membership в виде массива id.После того, как мы получим от доктрины наш недогидрированный агрегат, нам останется только запросить теги из базы по
NonHydratedTagCollection::getIds() и вызвать метод NonHydratedTagCollection::hydrate().#гидрация #Doctrine
👍4
На этих выходных получил приглашение на презентацию нового Tank 400, проходившую за городом и предполагавшую тест-драйв по внедорожному маршруту, фуршет с барбекю и какие-то конкурсы и активности для всей семьи.
Поскольку новые машины я люблю, решил поехать, уговорив жену и детей присоединиться. И знаете, получился на удивление приятный досуг выходного дня и, что удивительно, совершенно бесплатный (хотя за халявой я никогда не гнался и был бы даже готов заплатить за билет на сие мероприятие).
Внедорожный тест-драйв оказался довольно приятной поездкой по живописному лесу с пересечением замерзших болота и маленькой речки. Для детей были организованы мастер-классы по изготовлению каких-то фигурок, шоперов и прочих финтифлюшек.
Завершили мы это мероприятие тремя видами шашлыка и чаем со всякими булочками-круасанами. В общем, ни разу не пожалели, что съездили. Для меня, если честно, стало откровением, что маркетинговые мероприятия-заманухи могут конкурировать по приятности времяпрепровождения с оплачиваемым досугом.
И нет, это не было рекламой Танка.😄
#отдых_от_работы
Поскольку новые машины я люблю, решил поехать, уговорив жену и детей присоединиться. И знаете, получился на удивление приятный досуг выходного дня и, что удивительно, совершенно бесплатный (хотя за халявой я никогда не гнался и был бы даже готов заплатить за билет на сие мероприятие).
Внедорожный тест-драйв оказался довольно приятной поездкой по живописному лесу с пересечением замерзших болота и маленькой речки. Для детей были организованы мастер-классы по изготовлению каких-то фигурок, шоперов и прочих финтифлюшек.
Завершили мы это мероприятие тремя видами шашлыка и чаем со всякими булочками-круасанами. В общем, ни разу не пожалели, что съездили. Для меня, если честно, стало откровением, что маркетинговые мероприятия-заманухи могут конкурировать по приятности времяпрепровождения с оплачиваемым досугом.
И нет, это не было рекламой Танка.
#отдых_от_работы
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4
Суррогатные сущности и суррогатные id при использовании Доктрины
Проблема с суррогатными id возникает при мапинге объектов-значений. Дело в том, что Доктрина не позволяет мапить классы на таблицы без первичного ключа. А какой первичный ключ может быть у объекта-значения?
Можно попробовать создать первичный ключ по полям нашего Value Object. Но, как правило, это невозможно: нет гарантированно уникальных полей или их сочетаний.
В книге Domain-Driven Design in PHP предлагается такой подход для решения этой проблемы:
⚙️ Описываем нужный нам класс Value Object в доменном слое, так, как того требует бизнес-логика, не задумываясь, как мы будем его хранить (короче, делаем по DDD).
⚙️ В инфраструктурном слое создаем класс-наследник этого Value Object.
⚙️ В классе-наследнике добавляем поле
⚙️ Мапим доктрину не на доменный класс, а на инфраструктурный класс-наследник. Поскольку, этот класс уже лежит в инфраструктурном слое, то можно не делать xml mapping, а сразу размечать этот класс атрибутами.
Я попробовал этот подход в паре-тройке проектов и нашел его все-таки избыточно трудозатратным. На мой взгляд, здесь плата за то, чтобы писать совсем по фен-шую, слишком велика.
Для себя я выработал компромиссный подход:
⚙️ Поле
⚙️ Это поле помечается комментарием
⚙️ Поле приватное, геттеров и сеттеров нет, Шторм его подсвечивает как неиспользуемое.
⚙️ В xml маппинге указываем Доктрине, что это поле является автогенерируемым первичным ключом, чтобы она отстала от нас со своей ошибкой, что entity не может быть без id.
⚙️ При гидрации доктрина воспользуется рефлексией и в памяти PHP процесса наши Value Objects будут с айдишниками, что можно увидеть с помощью xDebug.
Да, при таком подходе немного страдает "феншуйность", но, на мой взгляд, это отклонение от DDD прям совсем незначительное и не стоит того, чтобы платить за его устранение усложнением проекта в виде создания суррогатных сущностей.
Тем не менее, есть ситуации, когда создание суррогатных сущностей полезно. То есть, в доменном слое у нас чисто доменные модели, не замутненные мапингом (он у нас в xml, как я писал выше), а в инфраструктурном слое есть свои классы - Доктриновские entity, про которые доменный слой ничего не знает. Вот пара примеров, когда мне это было полезно:
☘️ При организации nested sets средствами gedmo/doctrine-extensions. Суррогатной сущностью в этом кейсе у меня был класс категорий, который нес в себе кучу полей для организации дерева. Доменному слою вся эта шелуха не требовалось, требовался только
☘️ При реализации таблицы енамов. Кто не знал, объявление енамов на уровне SQL - антипаттерн (см. книгу Билла Карвина). Решение - держать значение енама в отдельной таблице, а целостность данных контролировать внешним ключом, ссылающимся на эту таблицу. Для реализации такого решения таблицу, содержащую значения енама, можно описать суррогатной сущностью.
☘️ При реализации паттерна Криса Ричардсона Transactional outbox. Сохраняемое в базу сообщение не является частью доменного слоя и описать структуру таблицы для его хранения можно с помощью суррогатной сущности.
Завершая серию постов о ручной гидрации, хочу сказать, что не такая она и страшная. Когда набиваешь руку, затраты времени на ее реализацию становятся довольно незначительными. Наградой же нам становится чистота доменного слоя и возможность писать бизнес-логику, не задумываясь о том, как будут сохраняться в базу наши сущности и объекты-значения.
На этом с ручной гидрацией все. С завтрашнего дня пойдут паттерны "Банды четырех".😃
#гидрация #Doctrine
Проблема с суррогатными id возникает при мапинге объектов-значений. Дело в том, что Доктрина не позволяет мапить классы на таблицы без первичного ключа. А какой первичный ключ может быть у объекта-значения?
Можно попробовать создать первичный ключ по полям нашего Value Object. Но, как правило, это невозможно: нет гарантированно уникальных полей или их сочетаний.
В книге Domain-Driven Design in PHP предлагается такой подход для решения этой проблемы:
⚙️ Описываем нужный нам класс Value Object в доменном слое, так, как того требует бизнес-логика, не задумываясь, как мы будем его хранить (короче, делаем по DDD).
⚙️ В инфраструктурном слое создаем класс-наследник этого Value Object.
⚙️ В классе-наследнике добавляем поле
id.⚙️ Мапим доктрину не на доменный класс, а на инфраструктурный класс-наследник. Поскольку, этот класс уже лежит в инфраструктурном слое, то можно не делать xml mapping, а сразу размечать этот класс атрибутами.
Я попробовал этот подход в паре-тройке проектов и нашел его все-таки избыточно трудозатратным. На мой взгляд, здесь плата за то, чтобы писать совсем по фен-шую, слишком велика.
Для себя я выработал компромиссный подход:
⚙️ Поле
id объявляется в доменном классе Value Object.⚙️ Это поле помечается комментарием
/** surrogate */.⚙️ Поле приватное, геттеров и сеттеров нет, Шторм его подсвечивает как неиспользуемое.
⚙️ В xml маппинге указываем Доктрине, что это поле является автогенерируемым первичным ключом, чтобы она отстала от нас со своей ошибкой, что entity не может быть без id.
⚙️ При гидрации доктрина воспользуется рефлексией и в памяти PHP процесса наши Value Objects будут с айдишниками, что можно увидеть с помощью xDebug.
Да, при таком подходе немного страдает "феншуйность", но, на мой взгляд, это отклонение от DDD прям совсем незначительное и не стоит того, чтобы платить за его устранение усложнением проекта в виде создания суррогатных сущностей.
Тем не менее, есть ситуации, когда создание суррогатных сущностей полезно. То есть, в доменном слое у нас чисто доменные модели, не замутненные мапингом (он у нас в xml, как я писал выше), а в инфраструктурном слое есть свои классы - Доктриновские entity, про которые доменный слой ничего не знает. Вот пара примеров, когда мне это было полезно:
☘️ При организации nested sets средствами gedmo/doctrine-extensions. Суррогатной сущностью в этом кейсе у меня был класс категорий, который нес в себе кучу полей для организации дерева. Доменному слою вся эта шелуха не требовалось, требовался только
name категории.☘️ При реализации таблицы енамов. Кто не знал, объявление енамов на уровне SQL - антипаттерн (см. книгу Билла Карвина). Решение - держать значение енама в отдельной таблице, а целостность данных контролировать внешним ключом, ссылающимся на эту таблицу. Для реализации такого решения таблицу, содержащую значения енама, можно описать суррогатной сущностью.
☘️ При реализации паттерна Криса Ричардсона Transactional outbox. Сохраняемое в базу сообщение не является частью доменного слоя и описать структуру таблицы для его хранения можно с помощью суррогатной сущности.
Завершая серию постов о ручной гидрации, хочу сказать, что не такая она и страшная. Когда набиваешь руку, затраты времени на ее реализацию становятся довольно незначительными. Наградой же нам становится чистота доменного слоя и возможность писать бизнес-логику, не задумываясь о том, как будут сохраняться в базу наши сущности и объекты-значения.
На этом с ручной гидрацией все. С завтрашнего дня пойдут паттерны "Банды четырех".
#гидрация #Doctrine
Please open Telegram to view this post
VIEW IN TELEGRAM
Leanpub
Domain-Driven Design in PHP
Master Domain-Driven Design Tactical patterns: Entities, Value Objects, Services, Domain Events, Aggregates, Factories, Repositories and Application Services.
🔥1
Паттерн Композит
Главное, что нужно понимать про паттерн Композит, это то, что он позволяет работать единообразно как с целой структурой, так и с каждой составной ее частью. То есть, в клиентском коде нам нет нужды различать, обращаемся мы ко всей структуре, или к отдельному ее элементу: как часть, так и целое имеют общий интерфейс.
Допустим, мы описываем структуру базы знаний, подобную файловой системе. У нас есть материалы базы знаний, которые разложены по папкам. Далее у нас возникает потребность выполнять с материалами какие-либо действия: отмечать, что материал просмотрен пользователем, переносить его в черновики, вешать на него какие-нибудь теги и т. д.
Нам бы хотелось, чтобы все действия, которые применяются к материалам, могли бы применяться и к папкам. Действие, примененное к папке, должно применяться ко всему ее содержимому.
В будущем мы планируем развивать структуру базы знаний, добавляя в нее такие сущности, как тест, курс, видеоурок и т. д. И ко всем этим сущностям нам тоже будет требоваться применять описанные выше действия.
Для решения такого рода задач как раз и подходит Композит: он позволяет объявить единый интерфейс операций, которые мы хотим выполнять над объектами нашей базы знаний, и реализовать эти операции так, что снаружи нам будет все равно, выполняем мы действие над всей базой знаний (корень структуры), какой-то отдельной папкой (узловой элемент) или над одним материалом (листовой элемент).
Давайте представим, что у нас есть бизнес-логика публикации нового материала, теста или сразу целой папки со всем содержимым. Публикация означает смену статуса с
Поскольку все элементы базы знаний наследуют единый интерфейс, мы можем "скармливать" методу🙂
Вот как мог бы выглядеть код наших элементов базы знаний:
Благодаря такому подходу мы можем строить структуры любой вложенности и работать единообразно как с ее частями, так и со всей структурой целиком, не "заморачивая" клиентский код знанием о том, с чем он сейчас работает: с корнем, узлом или листом.
#Паттерны
Главное, что нужно понимать про паттерн Композит, это то, что он позволяет работать единообразно как с целой структурой, так и с каждой составной ее частью. То есть, в клиентском коде нам нет нужды различать, обращаемся мы ко всей структуре, или к отдельному ее элементу: как часть, так и целое имеют общий интерфейс.
Допустим, мы описываем структуру базы знаний, подобную файловой системе. У нас есть материалы базы знаний, которые разложены по папкам. Далее у нас возникает потребность выполнять с материалами какие-либо действия: отмечать, что материал просмотрен пользователем, переносить его в черновики, вешать на него какие-нибудь теги и т. д.
Нам бы хотелось, чтобы все действия, которые применяются к материалам, могли бы применяться и к папкам. Действие, примененное к папке, должно применяться ко всему ее содержимому.
В будущем мы планируем развивать структуру базы знаний, добавляя в нее такие сущности, как тест, курс, видеоурок и т. д. И ко всем этим сущностям нам тоже будет требоваться применять описанные выше действия.
Для решения такого рода задач как раз и подходит Композит: он позволяет объявить единый интерфейс операций, которые мы хотим выполнять над объектами нашей базы знаний, и реализовать эти операции так, что снаружи нам будет все равно, выполняем мы действие над всей базой знаний (корень структуры), какой-то отдельной папкой (узловой элемент) или над одним материалом (листовой элемент).
Давайте представим, что у нас есть бизнес-логика публикации нового материала, теста или сразу целой папки со всем содержимым. Публикация означает смену статуса с
draft на active и навешивание тега novelty (новинка).
public function publish(WikiElemnt $element)
{
$element->activate();
$element->addTag(new Tag('novelty'));
}
Поскольку все элементы базы знаний наследуют единый интерфейс, мы можем "скармливать" методу
publish() что угодно: материалы, тесты и даже целые папки. Помните, я писал про полиморфизм? Это он. Вот как мог бы выглядеть код наших элементов базы знаний:
abstract class WikiElement
{
public function activate(): void
{
$this->status = WikiElementStatus::ACTIVE;
}
public function addTag(Tag $tag): void
{
$this->tags->add($tag);
}
}
class Material extends WikiElement
{
//...
}
class Test extends WikiElement
{
//...
}
class Folder extends WikiElement
{
/** @var WikiElement[] */
private array $elements = []
public function activate(): void
{
foreach ($this->elements as $element) {
$element->activate();
}
}
public function addTag(Tag $tag): void
{
foreach ($this->elements as $element) {
$element->addTag($tag);
}
}
}
Благодаря такому подходу мы можем строить структуры любой вложенности и работать единообразно как с ее частями, так и со всей структурой целиком, не "заморачивая" клиентский код знанием о том, с чем он сейчас работает: с корнем, узлом или листом.
#Паттерны
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Паттерн билдер: зачем нужен и где использовать
Ключевой, на мой взгляд, особенностью Билдера является класс Директора, о чем почему-то часто забывают, реализовывая только собственно Билдер.
В то время, как класс Билдера инкапсулирует различные этапы строительства объекта, Директор инкапсулирует логику строительства: то, какие этапы и в какой последовательности будут вызваны.
Давайте представим себе проект, В котором логика контроля доступа основывается на сущности
Чем больше у пользователя полномочий, тем сильнее насыщен разными данными его
При этом, не смотря на огромную гибкость
В классе
Знание о том, какие конкретные роли с определенными полномочиями будут порождаться, мы поместим в класс
Итого, в нашем решении есть несколько точек расширения:
🍀 Класс
🍀 Класс
🍀
Если вдруг в нашем проекте
Так и понимаем: в соответствии с выбранным архитектурным подходом и методологией разработки. Если вы решили, что согласно DDD логика построения😏
#Паттерны
Ключевой, на мой взгляд, особенностью Билдера является класс Директора, о чем почему-то часто забывают, реализовывая только собственно Билдер.
В то время, как класс Билдера инкапсулирует различные этапы строительства объекта, Директор инкапсулирует логику строительства: то, какие этапы и в какой последовательности будут вызваны.
Давайте представим себе проект, В котором логика контроля доступа основывается на сущности
ManagerAuthorities. Логики этой довольно много и потому ManagerAuthoities - агрегат, включающий в себя вложенные объекты, описывающие такие атрибуты, как вышестоящий менеджер, подотчетные менеджеру отделы, уровень ответственности менеджера (junior, middle, senior) и так далее.Чем больше у пользователя полномочий, тем сильнее насыщен разными данными его
ManagerAuthorities. Наоборот, у джуна, работающего в отделе, который пользуется лишь малой частью системы, ManagerAuthorities включает лишь пару полей.При этом, не смотря на огромную гибкость
ManagerAuthorities, нам бы хотелось иметь некий готовый набор ролей, наделенных конкретными видами полномочий. Вот для решения этой задачи и пригодится паттерн Билдер.В классе
ManagerAuthoritiesBuilder мы соберем различные операции по наделению менеджера каждым конкретным видом полномочий: seniorLevel(), middleLevel(), responsibleForAccauntant(), responsibleForMarketing() и т. д.Знание о том, какие конкретные роли с определенными полномочиями будут порождаться, мы поместим в класс
RoleKeeper. Этот класс и есть Директор из паттерна Билдер. Он знает, какие методы билдера и в каком порядке вызывать, возвращая в клиентский код готовый результат сборки.class RoleKeeper
{
private ManagerAuthoritiesBuilder $builder;
public function buyer(User $superiorManager): ManagerAuthorities
{
$this->builder->responsibleForBuying()
->middleLevel()
->subordinate($superiorManager);
return $this->builder->getResult();
}
public function salesManager(User $superiorManager): ManagerAuthorities
{
//...
}
}
Итого, в нашем решении есть несколько точек расширения:
ManagerAuthorities можно расширять независимо от остальных компонентов системы. Можно даже сделать его абстрактным и создать несколько подклассов, на что на самом деле и заточен "исконный" Билдер из первоисточника.ManagerAuthoritiesBuilder дополняется новыми этапами строительства ManagerAuthorities по мере расширения последнего. Если ManagerAuthorities является абстрактным и имеет потомков, то и ManagerAuthoritiesBuilder будет абстрактным со своими потомками на каждый подвид ManagerAuthorities.RoleKeeper можно дополнять бесконечным количеством методов по одному на каждую роль: buyer(), salesManager(), accountant() и так далее до бесконечности. При необходимости можно разбить класс RoleKeeper на несколько. Это никак не повлияет на остальные компоненты Билдера.Если вдруг в нашем проекте
ManagerAuthorities является доменным агрегатом, то вы можете не захотеть выносить этапы его строительства в отдельный класс ManagerAuthoritiesBuilder. Это уже к вопросу "а как именно мы понимаем, что есть единая ответственность", который следует у дотошного интервьюера после вашего ответа на вопрос о принципе Single responsibility.Так и понимаем: в соответствии с выбранным архитектурным подходом и методологией разработки. Если вы решили, что согласно DDD логика построения
ManagerAuthorities не может быть отделена от него, то и не отделяйте. В этом случае мы можем сказать, что наш кейс - вырожденный случай применения паттерна Билдер. #Паттерны
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7😁1
Паттерн Визитер. Синтетический пример.
Этот паттерн позволяет вынести из классов определенные операции в отдельный класс-Визитер. Это в свою очередь позволяет независимо развивать линейку классов-визитеров для выполнения разных вариантов одних и тех же операций. Давайте для начала рассмотрим синтетический пример (не волнуйтесь, реальный пример тоже будет).
Предположим, у нас есть корзинка с фруктами:
В этом примере фрукты - это классы, из которых были вынесены операции. Теперь, вместо вынесенных операций они реализуют метод
Развивая пример корзинки с фруктами: вместо методов
Теперь мы можем независимо объявлять новые классы фруктов и реализовывать разных визитеров для создания разных вариантов мойки и нарезки. А нашу корзинку с фруктами мы теперь можем обойти форычем и вызвать для каждого фрукта методы
Но на самом деле, лично меня паттерн визитер гораздо чаще выручал не в ситуациях, когда нужно отдельно развивать два дерева классов, а в ситуациях, когда нужно грамотно поделить ответственность между двумя классами, уменьшив coupling. Но об этом в следующем посте.😄
#Паттерны
Этот паттерн позволяет вынести из классов определенные операции в отдельный класс-Визитер. Это в свою очередь позволяет независимо развивать линейку классов-визитеров для выполнения разных вариантов одних и тех же операций. Давайте для начала рассмотрим синтетический пример (не волнуйтесь, реальный пример тоже будет).
Предположим, у нас есть корзинка с фруктами:
class Apple extends Fruit
{
public function acceptVisitor(Visitor $visitor){
$visitor->visitApple($this)
}
}
class Banana extends Fruit
{
public function acceptVisitor(Visitor $visitor){
$visitor->visitBanana($this)
}
}
class Visitor
{
public function visitApple(Apple $apple);
public function visitBanana(Banana $banana);
}
В этом примере фрукты - это классы, из которых были вынесены операции. Теперь, вместо вынесенных операций они реализуют метод
acceptVisitor(), внутри которого делегируют Визитеру выполнение операции над собой. Visitor же реализует внутри себя операции над конкретными классами в виде методов visitApple() и visitBanana().Развивая пример корзинки с фруктами: вместо методов
visitApple() и visitBanana() мы могли бы объявить операции washApple(), sliceApple(), washBanana(), sliceBanana() и т. д. В классах-фруктах у нас были бы методы wash(Visitor $visitor) и slice(Visitor $visitor).Теперь мы можем независимо объявлять новые классы фруктов и реализовывать разных визитеров для создания разных вариантов мойки и нарезки. А нашу корзинку с фруктами мы теперь можем обойти форычем и вызвать для каждого фрукта методы
wash() и slice(), передавая каждый раз нужный вариант визитера, в зависимости от того, какой вид нарезки и мойки нам нужен.Но на самом деле, лично меня паттерн визитер гораздо чаще выручал не в ситуациях, когда нужно отдельно развивать два дерева классов, а в ситуациях, когда нужно грамотно поделить ответственность между двумя классами, уменьшив coupling. Но об этом в следующем посте.
#Паттерны
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3