Подтвердить что ты не робот

Sf2: использование службы внутри объекта

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

Учитывая очень простой сервис:

Class Age
{
  private $date1;
  private $date2;
  private $format;

  const ym = "%y years and %m month"
  const ...


  // some DateTime()->diff() methods, checking, formating the entry formats, returning different period formats for eg.
  }

и простой объект:

Class People
{
  private $firstname;
  private $lastname;
  private $birthday;

  }

Из контроллера, который я хочу сделать:

$som1 = new People('Paul', 'Smith', '1970-01-01');
$som1->getAge();

Вне курса я могу переписать функцию getAge() внутри моей сущности, ее не долго, но я очень ленив, и поскольку я уже написал все возможные datetime- > diff(), которые мне нужны в вышеупомянутой службе, я не могу понять, почему я не должен использовать его...

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

Наследование кажется плохой "хорошей идеей", поскольку я мог использовать getAge() внутри класса BlogArticle, и я сомневаюсь, что этот класс BlogArticle должен наследоваться от того же класса, что и класс People...

Надеюсь, что я был ясен, но не уверен...

4b9b3361

Ответ 1

Одна большая путаница для многих программистов - думать, что сущности доктрины "являются" моделью. Это ошибка

  • См. Редактирование этого поста в конце, включая идеи, связанные с CQRS + ES -

Внедрение услуг в ваши сущности доктрины является симптомом "попытки сделать больше, чем хранение данных" в ваших сущностях. Когда вы видите этот "анти-шаблон", скорее всего, вы нарушаете принцип "Единой ответственности" в программировании SOLID.

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 should be a separated layer different from the persistence layer

  • Название 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: Используйте фабрики

Затем вы должны помнить, что потребители модели, никогда не должны создавать объекты модели, используя "новые". Вместо этого им следует использовать фабрику, которая правильно настроит объекты модели (свяжется с соответствующим слоем хранения данных). Единственное исключение - модульное тестирование (мы увидим это позже). Но кроме унитарных тестов, возьмите это с огнем в вашем мозгу и сделайте татуировку с помощью лазера на сетчатке: никогда не делайте "нового" в контроллере или команде. Используйте фабрики вместо этого;)

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

Use a service as a factory to get your model

Вы можете увидеть 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 действия под контролем благодаря событиям.

Example of throwing events from the model for virtually everything that can happen

  • Вы можете заметить, что в комплекте есть только Модель и Тесты.
  • В модели мы определяем наши 2 объекта: Game и GameManager, чтобы получить экземпляры объектов Game.
  • Мы также определяем интерфейсы, как, например, GameObserver, поэтому любой, кто хочет получать события Game, должен быть фолком GameObserver.
  • Затем вы можете увидеть, что для любого действия, которое изменяет состояние модели (например, добавление игрока), у меня есть 2 события: PRE и POST. Посмотри, как это работает:

    1. Кто-то вызывает метод $game-> addPlayer ($ player).
    2. Как только мы входим в функцию addPlayer(), возникает событие PRE.
    3. Затем наблюдатели могут поймать это событие, чтобы решить, можно ли добавить игрока или нет.
    4. Все события PRE должны сопровождаться отменой, переданной по ссылке. Поэтому, если кто-то решит, что это игра для 2 игроков, и вы попытаетесь добавить 3-го, для $ cancel будет установлено значение true.
    5. Тогда вы снова внутри функции addPlayer. Вы можете проверить, если кто-то хотел отменить операцию.
    6. Выполните операцию, если это разрешено (то есть: измените состояние $this->).
    7. После изменения состояния 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.

enter image description here

В нем есть стрелки, которые представляют поток.

В этом примере из двух стратегий внедрения, которые я вам говорил, я выбираю первую: вводить в объект модели Game необходимые сервисы (в данном случае BoardManager, PieceManager и ObjectStorageManager), а не внедрять сам GameManager.

  1. Во-первых, вы вызываете phpunit, который вызывает поиск каталога Tests, рекурсивно во всех каталогах, и находит классы с именем XxxTest. Затем будет желание вызвать все методы с именем textSomething().
  2. Но перед его вызовом для каждого метода тестирования он вызывает setup().
  3. В настройках мы создадим несколько тест-двойников, чтобы избежать "реального доступа" к базе данных при тестировании, при правильном тестировании логики в нашей модели. В этом случае двойник моего собственного менеджера уровня данных, ObjectStorageManager.
  4. Для ясности он назначен временной переменной...
  5. ... который хранится в экземпляре GameTest...
  6. ... для последующего использования в самом тесте.
  7. Затем переменная $ sut (тестируемая система) создается с помощью new команды, а не через менеджера. Вы помните, что я сказал, что тесты были исключением? Если вы используете менеджер (вы все еще можете), то здесь это не юнит-тест, а интеграционный тест, потому что тестирует два класса: менеджер и игру. В new команде мы подделываем все зависимости, которые есть у модели (как у менеджера доски и у менеджера фигуры). Я жестко кодирую GameId = 1 здесь. Это относится к сохранению данных, см. Ниже.
  8. Затем мы можем вызвать тестируемую систему (простой объект модели Game) для проверки ее внутренних компонентов.

Я жестко кодирую "Game id = 1" в new. В этом случае мы только проверяем, что возвращаемый тип является объектом DateTime. Но в случае, если мы хотим проверить также, что дата, которую он получает, является правильной, мы можем "настроить" макет ObjectStorageManager (слой устойчивости данных), чтобы он возвращал все, что мы хотим во внутреннем вызове, поэтому мы можем проверить это, например, когда я запрашиваю дату для уровня данных для игры = 1, дата - 1 июня 2014 года, а для игры = 2 - 2 июня 2014 года. Затем в testGetStartDate я бы создал 2 новых экземпляра с идентификаторами 1 и 2 и проверил бы содержимое результата.

В частности, в вашем проекте

У вас будет unit тест Test/Model/PersonTest, который сможет поиграть с логикой человека, а в случае необходимости человека из базы данных вы подделаете его через макет.

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

Короче:

  1. Не путайте логику и постоянство данных. Создайте Model, которая не имеет ничего общего с сущностями, и поместите в нее всю логику.
  2. Никогда не используйте new чтобы получить ваши модели от любого потребителя. Вместо этого используйте услуги фабрики. Особое внимание следует избегать новостей в контроллерах и командах. Исключение: unit тест является единственным потребителем, который может использовать new.
  3. Внедрите нужные вам сервисы в Модель через фабрику, которая, в свою очередь, получает ее из файла конфигурации services.yml.
  4. Бросай события для всего. Когда я говорю все, значит все. Представьте, что вы наблюдаете за моделью. Что бы вы хотели узнать? Добавьте событие для этого.
  5. Поймать события от контроллеров, представлений, команд и других частей модели, но, в частности, отловить их на уровне хранения данных, чтобы вы могли "копировать" объект на диск, не навязывая модели.
  6. Модульное тестирование вашей логики без зависимости от какой-либо реальной базы данных. Подключите реальную систему хранения базы данных к работе и добавьте фиктивную реализацию для ваших тестов.

Кажется, много работы. Но это не так. Это вопрос привыкания к нему. Просто подумайте о нужных "объектах", создайте их и сделайте слой данных "монитором" ваших объектов. Тогда ваши объекты могут свободно работать, отделены. Если вы создаете модель на заводе-изготовителе, добавьте в модель все необходимые службы в модели и оставьте данные в покое.

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 (он просто сообщает, что изменение на самом деле уже произошло, делайте с этой информацией все, что хотите).

Надеюсь помочь.

Хави.

Ответ 2

Вы уже упоминали очень хороший момент. Экземпляры класса Person - это не единственное, что может иметь возраст. BlogArticle может также возрасти вместе со многими другими типами. Если вы используете PHP 5.4+, вы можете использовать черты для добавления небольшого количества функциональных возможностей вместо того, чтобы иметь объекты обслуживания из контейнера (или, может быть, вы можете их комбинировать).

Вот быстрый макет того, что вы могли бы сделать, чтобы сделать его очень гибким. Это основная идея:

  • Укажите один возрастный признак (Aging)
  • Имейте конкретный признак, который может вернуть соответствующее поле ($birthdate, $createdDate,...)
  • Используйте свойство внутри вашего класса

Generic

trait Aging {
    public function getAge() { 
        return $this->calculate($this->start()); 
    }

    public function calculate($startDate) { ... }
}

Для человека

trait AgingPerson {
    use Aging;
    public function start() {
        return $this->birthDate;
    }   
}

class Person {
    use AgingPerson;
    private $birthDate = '1999-01-01'; 
}

Для статьи в блоге

// Use for articles, pages, news items, ...
trait AgingContent {
    use Aging;
    public function start() {
        return $this->createdDate;
    }   
}

class BlogArticle {
    use AgingContent;
    private $createDate = '2014-01-01'; 
}

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

echo (new Person())->getAge();
echo (new BlogArticle())->getAge();

Наконец

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

interface Ageable {
    public function getAge();
}

class Person implements Ageable { ... }
class BlogArticle implements Ageable { ... }

function doSomethingWithAgeable(Ageable $object) { ... }

Это может показаться много хлопот, когда на самом деле это намного проще поддерживать и расширять этот путь.

Ответ 3

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

$person = $personRepository->find(1); // How to get the age service injected?

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

$ageCalculator = $container('age_service');

$person = $personRepository->find(1);

$age = $person->calcAge($ageCalculator);

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

Похоже, что у вас может быть выходное форматирование? Вероятно, такого рода вещи можно сделать в веточке. getAge действительно должен просто вернуть число.

Аналогично, ваша дата рождения действительно должна быть объектом даты, а не строкой.

Ответ 4

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

  • Вы действительно можете создать суперкласс класса AbstractEntity, из которого наследуются все остальные объекты. Этот AbstractEntity будет содержать вспомогательные методы, которые могут понадобиться другим объектам.

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

  • Вы можете написать службу, отвечающую за сущность/объекты, о которых идет речь. Недостаток: вы не можете контролировать, что другие части вашего кода (или другие разработчики) знают об этой услуге. Преимущество: нет никаких ограничений на то, что вы можете сделать, и все это прекрасно оформлено.

  • Вы можете работать с События/обратные вызовы жизненного цикла.

  • Если вам действительно нужно вводить услуги в сущности, вы можете рассмотреть возможность установки статического свойства в сущности и только установить его один раз в контроллер или выделенный сервис. Тогда вам не нужно заботиться о каждой инициализации объекта. Может быть объединен с подходом AbstractEntity.

Как упоминалось ранее, все они имеют свои преимущества и недостатки. Выберите свой яд.