PHP & DDD: Как обеспечить, чтобы только служба могла вызывать метод на объекте?

Я работаю с моделью предметной области, в которой у меня есть класс резервирования:

class Reservation
{
    public function changeStatus($status) { ... }
}

Поскольку changeStatus() метод должен вызываться только в контексте, где отправляются все соответствующие уведомления (электронные письма, ...). Я бы хотел ограничить вызов этого метода ReservationService:

class ReservationService
{
    public function confirmReservation(Reservation $reservation)
    {
        $reservation->changeStatus(Reservation::STATUS_CONFIRMED);
        // commit changes to the db, send notifications, etc.
    }
}

Поскольку я работаю с PHP, нет такой концепции, как видимость пакета или классы друзей, поэтому мой changeStatus() Метод просто публичный и поэтому может вызываться из любого места приложения.

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

class Reservation
{
    public function changeStatus(ReservationService $service)
    {
        $status = $service->getReservationStatus($this);
        $this->setStatus($status);
    }

    protected function setStatus($status) { ... }
}

Потенциальные недостатки:

  • Это немного усложняет дизайн
  • Это делает организацию осведомленной о Сервисе, независимо от того, является ли это недостатком или нет.

Ребята, есть ли у вас какие-либо комментарии к вышеупомянутому решению, или лучший дизайн, чтобы предложить ограничить доступ к этому changeStatus() метод?

4 ответа

Решение

Одна из вещей, которую приняли платформы Symfony2 и FLOW3, - это пометить их стабильный публичный API с помощью @api комментарий к аннотации.

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

Пример:

class Request
{
    /**
     * Gets the Session.
     *
     * @return Session|null The session
     *
     * @api
     */
    public function getSession()
    {
        return $this->session;
    }
}

Используйте интерфейс, который обеспечивает необходимый вам контекст:

interface INotifiable {
  public function updated( $reservation );
}

class Reservation {
  public function changeStatus( $status, INotifiable $notifiable ){
    $this->setStatus( $status );
    $notifiable->updated( $this );
  }
}

class EmailNotifier implements INotifiable {
  public function updated( $reservation ){
    $this->sendUpdateEmail( $reservation ); //or whatever
  }
}

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

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

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

class Domain_ReservationService
{

    private $changeStatusRequestFactory;

    public function __construct(
        Message_Factory_ChangeStatusRequest $changeStatusRequestFactory
    ) {
        $this->changeStatusRequestFactory = $changeStatusRequestFactory;
    }

    public function confirmReservation(Domain_Reservation $reservation) {

        $changeStatusRequest = $changeStatusRequestFactory->make(
            Reservation::STATUS_CONFIRMED
        );

        $reservation->changeStatus($changeStatusRequest);
        // commit changes to the db, send notifications, etc.

    }

}

Объект Reservation проверяет содержимое сообщения и решает, что делать.

class Domain_Reservation
{

    public function changeStatus(
        Message_Item_ChangeStatusRequest $changeStatusRequest
    ) {
        $satus = $changeStatusRequest->getStatus();
        ...
    }

}

Объект сообщения является объектом значения DDD. (Иногда это действует как стратегия.)

class Message_Item_ChangeStatusRequest
{
    private $status;

    public function __construct( $status ) {
        $this->$status = $status;
    }

    public function getStatus() {
        return $this->$status;
    }

}

Эта фабрика производит сообщения.

class Message_Factory_ChangeStatusRequest
{

    public function make($status) {
        return new Message_Item_ChangeStatusRequest ($status);
    }

}

Все доменные объекты создаются этой фабрикой слоев.

class Domain_Factory
{

    public function makeReservationService() {
        return new Domain_ReservationService(
            new Message_Factory_ChangeStatusRequest()
        );
    }

    public function makeReservation() {
        return new Domain_Reservation();
    }

}

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

$factory = new Domain_Factory();
$reservationService = $factory->makeReservationService();
$reservation = $factory->makeReservation();
$reservationService->confirmReservation($reservation);

Но я не понимаю, почему вы не хотите использовать $ booking->beConfirmed() вместо передачи констант статуса.

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

Пример рабочего процесса:

  1. PlaceReservationCommand отправлено ReservationContext, который обрабатывает его и публикует ReservationWasPlacedEvent что ReservationProcessManager подписан на.
  2. ReservationProcessManager получает ReservationWasPlacedEvent и подтверждает, что он может перейти к следующему шагу. Я тогда отправляю NotfiyCustomerAboutReservationCommand к NotificationContext, Сейчас слушает ReservationNotificationFailedEvent а также ReservationNotificationSucceededEvent,
  3. Теперь ReservationProcessManager отправляет ConfirmReservationCommand к ReservationContext только когда он получил ReservationNotificationSucceededEvent,

Хитрость заключается в том, что вReservation, ProcessManager отвечает за отслеживание состояния этой бизнес-транзакции. Скорее всего, есть ReservationProcess Агрегат, который содержит такие статусы, как ReservationProcess::INITIATED, ReservationProcess::CUSTOMER_NOTIFICATION_REQUESTED, ReservationProcess::CUSTOMER_NOTIFIED, ReservationProcess::CONFIRMATION_REQUESTED а также ReservationProcess::CONFIRMED, Последнее состояние указывает конечное состояние, которое отмечает процесс как выполненный.

Другие вопросы по тегам