Ответ 1
Одна большая путаница для многих программистов - думать, что сущности доктрины "являются" моделью. Это ошибка
- См. Редактирование этого поста в конце, включая идеи, связанные с CQRS + ES -
Внедрение услуг в ваши сущности доктрины является симптомом "попытки сделать больше, чем хранение данных" в ваших сущностях. Когда вы видите этот "анти-шаблон", скорее всего, вы нарушаете принцип "Единой ответственности" в программировании SOLID.
- http://en.wikipedia.org/wiki/Anti-pattern
- http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
- http://en.wikipedia.org/wiki/Single_responsibility_principle)
Symfony - это не среда MVC
, а только среда VC
. Не хватает части M
Сущности доктрины (далее я буду называть их сущностями, см. Пояснение в конце) - это "уровень постоянства данных", а не "уровень модели". В SF есть много вещей для представлений, веб-контроллеров, командных контроллеров... но он не помогает при моделировании домена (http://en.wikipedia.org/wiki/Domain_model) - даже постоянный уровень - это Doctrine, а не Symfony.
Преодоление проблемы в SF2
Когда вам "нужны" сервисы на уровне данных, запускайте антипаттерн-предупреждение. Хранилище должно быть только системой "положить сюда - получить оттуда". Ничего больше.
Чтобы преодолеть эту проблему, вы должны внедрить сервисы в "логический уровень" (модель) и отделить его от "чистого хранилища" (уровень сохранения данных). Следуя принципу единой ответственности, поместите логику в одну сторону, а геттеры и сеттеры в mysql.
Решение состоит в том, чтобы создать отсутствующий слой Model
, отсутствующий в Symfony2, и сделать его "логическим" для объектов домена, полностью отделенных и отделенных от уровня сохранения данных, который знает, "как хранить" модель в База данных mysql с доктриной, или с redis, или просто с текстовым файлом.
Все эти системы хранения должны быть взаимозаменяемыми, и ваша Model
должна по-прежнему предоставлять те же общедоступные методы без каких-либо изменений для потребителя.
Вот как ты это делаешь:
Шаг 1: Отделить модель от постоянства данных
Для этого в вашем комплекте вы можете создать еще один каталог с именем Model
на уровне корня пакета (помимо tests
, DependencyInjection
и т.д.), Как в этом примере игры.
- Название
Model
не обязательно, Symfony ничего не говорит об этом. Вы можете выбрать все, что вы хотите. - Если ваш проект прост (скажем, один пакет), вы можете создать этот каталог внутри одного пакета.
- Если ваш проект имеет много пакетов, вы можете рассмотреть
- либо помещая модель в разные связки, либо
- или -as в примере image- используйте ModelBundle, который содержит все "объекты", которые нужны проекту (без интерфейсов, контроллеров, команд, только логика игры и ее тесты). В этом примере вы видите
ModelBundle
, предоставляющий логические концепции, такие какBoard
,Piece
илиTile
и многие другие, структуры в каталогах для ясности.
Специально для вашего вопроса
В вашем примере вы могли бы иметь:
Entity/People.php
Model/People.php
- Все, что связано с "store", должно идти внутри
Entity/People.php
- Пример: предположим, что вы хотите сохранить дату рождения как в поле даты-времени, так и в трех избыточных полях: year, month, day, из-за любой хитрости вещи, связанные с поиском или индексацией, которые не связаны с доменом (то есть не связаны с "логикой" человека). - Все, что связано с "логикой", должно идти внутри
Model/People.php
- Пример: как рассчитать,Model/People.php
ли человек совершеннолетия только сейчас, учитывая определенную дату рождения и страну, в которой он живет (что будет определять минимальный возраст), Как видите, это не имеет никакого отношения к постоянству.
Шаг 2: Используйте фабрики
Затем вы должны помнить, что потребители модели, никогда не должны создавать объекты модели, используя "новые". Вместо этого им следует использовать фабрику, которая правильно настроит объекты модели (свяжется с соответствующим слоем хранения данных). Единственное исключение - модульное тестирование (мы увидим это позже). Но кроме унитарных тестов, возьмите это с огнем в вашем мозгу и сделайте татуировку с помощью лазера на сетчатке: никогда не делайте "нового" в контроллере или команде. Используйте фабрики вместо этого;)
Для этого вы создаете сервис, который действует как "получатель" вашей модели. Вы создаете геттер как фабрику, доступную через сервис. Смотрите изображение:
Вы можете увидеть BoardManager.php там. Это фабрика. Он действует как основной добытчик всего, что связано с досками. В этом случае BoardManager имеет следующие методы:
public function createBoardFromScratch( $width, $height )
public function loadBoardFromJson( $document )
public function loadBoardFromTemplate( $boardTemplate )
public function cloneBoard( $referenceBoard )
- Затем, как вы видите на изображении, в services.yml вы определяете этого менеджера и внедряете в него слой постоянства. В этом случае вы
ObjectStorageManager
вBoardManager
.ObjectStorageManager
, в этом примере, может хранить и загружать объекты из базы данных или из файла; в то время какBoardManager
зависит от хранилища. - Вы также можете увидеть
ObjectStorageManager
на изображении, который, в свою очередь, вводит @doctrine для доступа кmysql
. - Ваши менеджеры - единственное место, где разрешено
new
. Никогда в контроллере или команде.
Специально для вашего вопроса
В вашем примере у вас будет PeopleManager
в модели, способный получать объекты людей, как вам нужно.
Также в модели вы должны использовать правильные имена в единственном и множественном числе, так как они отделены от вашего уровня постоянства данных. Кажется, вы используете People
для представления одного Person
- это может быть потому, что вы (ошибочно) сопоставляете модель с именем таблицы базы данных.
Итак, задействованные модельные классы будут:
PeopleManager -> the factory
People -> A collection of persons.
Person -> A single person.
Например (псевдокод! Используя нотацию C++, чтобы указать тип возвращаемого значения):
PeopleManager
{
// Examples of getting single objects:
Person getPersonById( $personId ); -> Load it from somewhere (mysql, redis, mongo, file...)
Person ClonePerson( $referencePerson ); -> Maybe you need or not, depending on the nature the your problem that your program solves.
Person CreatePersonFromScratch( $name, $lastName, $birthDate ); -> returns a properly initialized person.
// Examples of getting collections of objects:
People getPeopleByTown( $townId ); -> returns a collection of people that lives in the given town.
}
People implements ArrayObject
{
// You could overload assignment, so you can throw an exception if any non-person object is added, so you can always rely on that People contains only Person objects.
}
Person
{
private $firstname;
private $lastname;
private $birthday;
}
Итак, продолжая ваш пример, когда вы делаете...
// **Never ever** do a new from a controller!!!
$som1 = new People('Paul', 'Smith', '1970-01-01');
$som1->getAge();
... теперь вы можете изменить:
// Use factory services instead:
$peopleManager = $this->get( 'myproject.people.manager' );
$som1 = $peopleManager->createPersonFromScratch( 'Paul', 'Smith', '1970-01-01' );
$som1->getAge();
PeopleManager сделает new
для вас.
На этом этапе ваша переменная $som1
типа Person
, как она была создана на заводе, может быть предварительно заполнена необходимой механикой для сохранения и сохранения в слое постоянства.
myproject.people.manager
будет определен в вашем services.yml и будет иметь доступ к доктрине либо напрямую, либо через слой "myproject.persistence.manager", либо как угодно.
Примечание. Эта инъекция уровня персистентности через менеджера имеет несколько побочных эффектов, которые могут отойти от "как сделать модель доступной к сервисам". Смотрите шаги 4 и 5 для этого.
Шаг 3: Введите необходимые вам услуги через фабрику.
Теперь вы можете добавлять любые нужные вам сервисы в people.manager
Вы, если ваш объект модели должен получить доступ к этой службе, у вас есть 2 варианта:
- Когда фабрика создает объект модели (то есть, когда PeopleManager создает Person), чтобы внедрить его через конструктор или сеттер.
- Проксируйте функцию в PeopleManager и внедрите PeopleManager через конструктор или установщик.
В этом примере мы предоставляем PeopleManager сервис, который будет использоваться моделью. Когда у менеджера персонала запрашивается новый объект модели, он добавляет сервис, необходимый ему, в new
предложении, чтобы объект модели мог напрямую обращаться к внешнему сервису.
// Example of injecting the low-level service.
class PeopleManager
{
private $externalService = null;
class PeopleManager( ServiceType $externalService )
{
$this->externalService = $externalService;
}
public function CreatePersonFromScratch()
{
$externalService = $this->externalService;
$p = new Person( $externalService );
}
}
class Person
{
private $externalService = null;
class Person( ServiceType $externalService )
{
$this->externalService = $externalService;
}
public function ConsumeTheService()
{
$this->externalService->nativeCall(); // Use the external API.
}
}
// Using it.
$peopleManager = $this->get( 'myproject.people.manager' );
$person = $peopleManager->createPersonFromScratch();
$person->consumeTheService()
В этом примере мы предоставляем PeopleManager сервис, который будет использоваться моделью. Тем не менее, когда менеджеру персонала запрашивают новый объект модели, он внедряет себя в созданный объект, поэтому объект модели может получить доступ к внешней службе через менеджера, который затем скрывает API, поэтому, если когда-либо внешняя служба изменит API, менеджер может сделать правильные преобразования для всех потребителей в модели.
// Second example. Using the manager as a proxy.
class PeopleManager
{
private $externalService = null;
class PeopleManager( ServiceType $externalService )
{
$this->externalService = $externalService;
}
public function createPersonFromScratch()
{
$externalService = $this->externalService;
$p = new Person( $externalService);
}
public function wrapperCall()
{
return $this->externalService->nativeCall();
}
}
class Person
{
private $peopleManager = null;
class Person( PeopleManager $peopleManager )
{
$this->peopleManager = $peopleManager ;
}
public function ConsumeTheService()
{
$this->peopleManager->wrapperCall(); // Use the manager to call the external API.
}
}
// Using it.
$peopleManager = $this->get( 'myproject.people.manager' );
$person = $peopleManager->createPersonFromScratch();
$person->ConsumeTheService()
Шаг 4: Бросай события для всего
На данный момент вы можете использовать любой сервис в любой модели. Кажется, все сделано.
Тем не менее, когда вы реализуете его, вы обнаружите проблемы при разъединении модели с сущностью, если вам нужен действительно SOLID шаблон. Это также относится к отделению этой модели от других частей модели.
Проблема явно возникает в таких местах, как "когда делать flush()" или "когда решать, нужно ли что-то сохранять или оставить для последующего сохранения" (особенно в долгоживущих процессах PHP), а также в проблемных изменениях в В случае, если доктрина меняет свой API и тому подобное.
Но это также верно, когда вы хотите проверить Лица без проверки его Дома, но Дом должен "отслеживать", меняет ли Человек свое имя, чтобы изменить имя в почтовом ящике. Это специально для долгоживущих процессов.
Решением этой проблемы является использование шаблона наблюдателя (http://en.wikipedia.org/wiki/Observer_pattern), чтобы объекты вашей модели генерировали события почти для чего угодно, а наблюдатель решает кэшировать данные в ОЗУ, заполнять данные или сохранять данные на диск.
Это сильно усиливает принцип сплошной/замкнутый. Вы никогда не должны менять свою модель, если вещь, которую вы меняете, не связана с доменом. Например, добавление нового способа хранения в базу данных нового типа, должно потребовать нулевую редакцию в ваших модельных классах.
Вы можете увидеть пример этого на следующем изображении. В нем я выделяю пакет под названием "TurnBasedBundle", который походит на основной функционал для каждой игры, основанной на пошаговой игре, несмотря на то, есть ли у нее доска или нет. Вы можете видеть, что в комплекте есть только Модель и Тесты.
В каждой игре есть набор правил, игроки, и во время игры игроки выражают желание того, что они хотят сделать.
В объекте Game
экземпляры добавят набор правил (poker "chess" tic-tac-toe "). Внимание: что если набор правил, который я хочу загрузить, не существует?
При инициализации кто-то (возможно, контроллер /start) добавит игроков. Осторожно: что если в игре участвуют 2 игрока, а я добавляю троих?
А во время игры контроллер, который получает движения игроков, будет добавлять желания (например, если он играет в шахматы, "игрок хочет переместить ферзя в эту плитку" -which может быть допустимым, или not-.
На картинке вы можете видеть эти 3 действия под контролем благодаря событиям.
- Вы можете заметить, что в комплекте есть только Модель и Тесты.
- В модели мы определяем наши 2 объекта: Game и GameManager, чтобы получить экземпляры объектов Game.
- Мы также определяем интерфейсы, как, например, GameObserver, поэтому любой, кто хочет получать события Game, должен быть фолком GameObserver.
-
Затем вы можете увидеть, что для любого действия, которое изменяет состояние модели (например, добавление игрока), у меня есть 2 события:
PRE
иPOST
. Посмотри, как это работает:- Кто-то вызывает метод $game-> addPlayer ($ player).
- Как только мы входим в функцию addPlayer(), возникает событие
PRE
. - Затем наблюдатели могут поймать это событие, чтобы решить, можно ли добавить игрока или нет.
- Все события
PRE
должны сопровождаться отменой, переданной по ссылке. Поэтому, если кто-то решит, что это игра для 2 игроков, и вы попытаетесь добавить 3-го, для $ cancel будет установлено значение true. - Тогда вы снова внутри функции addPlayer. Вы можете проверить, если кто-то хотел отменить операцию.
- Выполните операцию, если это разрешено (то есть: измените состояние $this->).
- После изменения состояния
POST
событиеPOST
чтобы указать наблюдателям, что операция была завершена.
На картинке вы видите три, но, конечно, их намного больше. Как правило, у вас будет около 2 событий на сеттер, 2 события на метод, которые могут изменять состояние модели, и 1 событие на каждое "неизбежное" действие. Так что если у вас есть 10 методов для класса, которые работают с ним, вы можете ожидать около 15 или 20 событий.
Вы можете легко увидеть это в типичном простом текстовом поле любой графической библиотеки любой операционной системы: Типичными событиями будут: gotFocus, lostFocus, keyPress, keyDown, keyUp, mouseDown, mouseMove и т.д.
В частности, в вашем примере
Человек будет иметь что-то вроде preChangeAge, postChangeAge, preChangeName, postChangeName, preChangeLastName, postChangeLastName, если у вас есть установщики для каждого из них.
Для долгоживущих действий, таких как "человек, ходите 10 секунд", у вас может быть 3: preStartWalking, postStartWalking, postStopWalking (в случае, если остановка в 10 секунд не может быть программно предотвращена).
Если вы хотите упростить, вы можете иметь два отдельных preChanged( $what, & $cancel )
и postChanged( $what )
для всего.
Если вы никогда не препятствуете изменениям, вы можете даже changed()
одно единственное событие changed()
для всех и любые изменения в вашей модели. Тогда ваша сущность будет просто "копировать" свойства модели в свойствах сущности при каждом изменении. Это нормально для простых классов и проектов или для структур, которые вы не собираетесь публиковать для сторонних потребителей, и экономит некоторое кодирование. Если класс модели станет основным классом для вашего проекта, потратив немного времени на добавление всего списка событий, вы сэкономите время в будущем.
Шаг 5: поймать события из слоя данных.
Именно в этот момент ваш пакет данных начинает действовать !!!
Сделайте ваш слой данных наблюдателем вашей модели. Когда модель меняет свое внутреннее состояние, тогда заставьте вашу сущность "копировать" это состояние в состояние сущности.
В этом случае MVC действует так, как ожидается: контроллер работает с моделью. Последствия этого все еще скрыты от контроллера (поскольку у контроллера не должно быть доступа к Doctrine). Модель "транслирует" выполненную операцию, так что все, кто интересуется, знают, что, в свою очередь, приводит к тому, что уровень данных знает об изменении модели.
В частности, в вашем проекте
Объект Model/Person
будет создан PeopleManager
. При его создании PeopleManager
, который является службой и, следовательно, может включать другие службы, может иметь под ObjectStorageManager
подсистему ObjectStorageManager
. Таким образом, PeopleManager
может получить Entity/People
, которых вы указали в своем вопросе, и добавить эту Entity/People
в качестве наблюдателя в Model/Person
.
В Entity/People
вы в основном заменяете всех сеттеров на ловушки событий.
Вы читаете свой код следующим образом: Когда Model/Person
меняет свое Фамилию, Entity/People
будут уведомлены и скопируют данные во внутреннюю структуру.
Скорее всего, у вас возникает соблазн внедрить сущность внутри модели, поэтому вместо вызова события вы вызываете установщики сущности.
Но при таком подходе вы "нарушаете" принцип Open-Closed. Поэтому, если в какой-то момент вы хотите перейти на MongoDb, вам необходимо "изменить" ваши "сущности" на "документы" в вашей модели. С шаблоном наблюдателя это изменение происходит за пределами модели, которая никогда не знает природу наблюдателя, за исключением того, что он является PersonObserver.
Шаг 6: Модульное тестирование всего
Наконец, вы хотите провести модульное тестирование своего программного обеспечения. Поскольку этот шаблон, который я объяснил, преодолевает обнаруженный вами анти-шаблон, вы можете (и должны) модульно тестировать логику своей модели независимо от того, как она хранится.
Следование этому шаблону помогает вам следовать принципам SOLID, поэтому каждая "единица кода" не зависит от других. Это позволит вам создавать модульные тесты, которые будут проверять "логику" вашей Model
без записи в базу данных, так как она внедрит поддельный слой хранения данных как test-double.
Позвольте мне снова использовать пример игры. Я показываю вам на картинке игровой тест. Предположим, что все игры могут длиться несколько дней, а начальная дата и время хранятся в базе данных. В этом примере мы тестируем только в том случае, если getStartDate() возвращает объект dateTime.
В нем есть стрелки, которые представляют поток.
В этом примере из двух стратегий внедрения, которые я вам говорил, я выбираю первую: вводить в объект модели Game
необходимые сервисы (в данном случае BoardManager
, PieceManager
и ObjectStorageManager
), а не внедрять сам GameManager
.
- Во-первых, вы вызываете phpunit, который вызывает поиск каталога Tests, рекурсивно во всех каталогах, и находит классы с именем XxxTest. Затем будет желание вызвать все методы с именем textSomething().
- Но перед его вызовом для каждого метода тестирования он вызывает setup().
- В настройках мы создадим несколько тест-двойников, чтобы избежать "реального доступа" к базе данных при тестировании, при правильном тестировании логики в нашей модели. В этом случае двойник моего собственного менеджера уровня данных,
ObjectStorageManager
. - Для ясности он назначен временной переменной...
- ... который хранится в экземпляре GameTest...
- ... для последующего использования в самом тесте.
- Затем переменная $ sut (тестируемая система) создается с помощью
new
команды, а не через менеджера. Вы помните, что я сказал, что тесты были исключением? Если вы используете менеджер (вы все еще можете), то здесь это не юнит-тест, а интеграционный тест, потому что тестирует два класса: менеджер и игру. Вnew
команде мы подделываем все зависимости, которые есть у модели (как у менеджера доски и у менеджера фигуры). Я жестко кодирую GameId = 1 здесь. Это относится к сохранению данных, см. Ниже. - Затем мы можем вызвать тестируемую систему (простой объект модели
Game
) для проверки ее внутренних компонентов.
Я жестко кодирую "Game id = 1" в new
. В этом случае мы только проверяем, что возвращаемый тип является объектом DateTime. Но в случае, если мы хотим проверить также, что дата, которую он получает, является правильной, мы можем "настроить" макет ObjectStorageManager (слой устойчивости данных), чтобы он возвращал все, что мы хотим во внутреннем вызове, поэтому мы можем проверить это, например, когда я запрашиваю дату для уровня данных для игры = 1, дата - 1 июня 2014 года, а для игры = 2 - 2 июня 2014 года. Затем в testGetStartDate я бы создал 2 новых экземпляра с идентификаторами 1 и 2 и проверил бы содержимое результата.
В частности, в вашем проекте
У вас будет unit тест Test/Model/PersonTest
, который сможет поиграть с логикой человека, а в случае необходимости человека из базы данных вы подделаете его через макет.
Если вы хотите проверить сохранение человека в базе данных, достаточно, чтобы вы провели юнит-тестирование, чтобы событие было выброшено, независимо от того, кто его слушает. Вы можете создать фальшивого слушателя, прикрепить к событию, и когда postChangeAge
произойдет, отметьте флаг и ничего не делайте (нет реального хранилища базы данных). Тогда вы утверждаете, что флаг установлен.
Короче:
- Не путайте логику и постоянство данных. Создайте
Model
, которая не имеет ничего общего с сущностями, и поместите в нее всю логику. - Никогда не используйте
new
чтобы получить ваши модели от любого потребителя. Вместо этого используйте услуги фабрики. Особое внимание следует избегать новостей в контроллерах и командах. Исключение: unit тест является единственным потребителем, который может использоватьnew
. - Внедрите нужные вам сервисы в Модель через фабрику, которая, в свою очередь, получает ее из файла конфигурации services.yml.
- Бросай события для всего. Когда я говорю все, значит все. Представьте, что вы наблюдаете за моделью. Что бы вы хотели узнать? Добавьте событие для этого.
- Поймать события от контроллеров, представлений, команд и других частей модели, но, в частности, отловить их на уровне хранения данных, чтобы вы могли "копировать" объект на диск, не навязывая модели.
- Модульное тестирование вашей логики без зависимости от какой-либо реальной базы данных. Подключите реальную систему хранения базы данных к работе и добавьте фиктивную реализацию для ваших тестов.
Кажется, много работы. Но это не так. Это вопрос привыкания к нему. Просто подумайте о нужных "объектах", создайте их и сделайте слой данных "монитором" ваших объектов. Тогда ваши объекты могут свободно работать, отделены. Если вы создаете модель на заводе-изготовителе, добавьте в модель все необходимые службы в модели и оставьте данные в покое.
Edit apr/2016 - отделение домена от сопротивления
Все вхождения слова сущность в этом ответе относятся к "доктринальным сущностям", что вызывает путаницу у большинства программистов, между уровнем модели и уровнем персистентности, которые всегда должны быть разными.
- Доктрина - это инфраструктура, поэтому доктрина по определению находится за пределами модели.
- Учение имеет сущности. Итак, по определению, тогда сущности доктрины также находятся вне модели.
- Вместо этого, растущая популярность строительных блоков
DDD
заставляет еще больше уточнить мой ответ, посколькуDDD
использует словоEntity
в модели. -
Domain objects
Domain entities
(неDoctrine entities
) аналогичны тем, что я упоминаю в этом ответе дляDomain objects
. - На самом деле, существует много типов
Domain objects
:-
Domain entities
(отличные отDoctrine entites
). -
Domain value objects
(могут быть схожи с базовыми типами с логикой) -
Domain events
(также отличные отSymfony events
а такжеDoctrine events
). -
Domain commands
(отличаются от тех помощников, подобных контроллеруSymfony command line
). -
Domain services
(отличные отSymfony framework services
). - и т.п.
-
- Поэтому примите все мое объяснение так: когда я говорю "сущности не являются модельными объектами", просто читайте "сущности доктрины не являются сущностями домена".
Edit июнь /2019 - CQRS + ES аналогия
Древние уже использовали постоянные методы истории, чтобы записывать вещи (например, ставить отметки на камне для регистрации транзакций).
На протяжении десятилетия подход CQRS + ES (разделение ответственности по запросам команд + выделение событий) в программировании набирает популярность, привнося идею "история неизменна" в программы, которые мы кодируем, и сегодня многие программисты думают об отделении команды. сторона против стороны запроса. Если вы не знаете, о чем я говорю, не беспокойтесь, просто пропустите следующие абзацы.
Растущая популярность CQRS + ES в последние 3 или 4 года заставляет меня задуматься над комментарием здесь и его отношением к тому, что я ответил здесь 5 лет назад:
Этот ответ рассматривался как одна отдельная модель, а не модель записи и модель чтения. Но я рад видеть много совпадающих идей.
Думайте о событиях PRE, которые я здесь упоминаю, как о "командах и модели записи". Думайте о событиях POST как о "части событий, идущей к модели чтения".
В CQRS вы можете легко обнаружить, что "команды могут быть приняты или нет" в зависимости от внутреннего состояния. Обычно их реализуют, создавая исключения, но есть и другие альтернативы, например, ответ на вопрос, была ли принята команда или нет.
Например, в "Поезде" я могу "установить его на скорость X". Но если состояние состоит в том, что поезд находится в рельсе, который не может идти дальше 80 км/ч, то установка его на 200 должна быть отклонена.
Это АНАЛОГОВО для логического значения cancel
переданного по ссылке, когда сущность может просто "отклонить" что-то ДО изменения своего состояния.
Вместо этого события POST не переносят событие "отмена" и генерируются ПОСЛЕ того, как произошло изменение состояния. Вот почему вы не можете отменить их: они говорят об "изменении состояния, которое действительно произошло", и поэтому его нельзя отменить: оно уже произошло.
Так...
В моем ответе 2014 года события "pre" совпадают с "принятием команды" систем CQRS + ES (команда может быть принята или отклонена), а события "post" совпадают с "событиями домена" CQRS + Системы ES (он просто сообщает, что изменение на самом деле уже произошло, делайте с этой информацией все, что хотите).
Надеюсь помочь.
Хави.