Pergunta

I'm working with a domain model, in which I have a Reservation class:

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

Because the changeStatus() method should only be called in a context where all appropriate notifications are sent (emails, ...) I would like to restrict the call to this method to a ReservationService:

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

Because I'm working with PHP, there is no such concept as package visibility or friend classes, so my changeStatus() method is just public and therefore callable from anywhere in the application.

The only solution I found to this problem, is to use some kind of double dispatch:

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

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

The potential drawbacks are:

  • That complicates the design a bit
  • That makes the entity aware of the Service, no sure whether that's actually a drawback or not

Do you guys have any comment on the above solution, or a better design to suggest to restrict access to this changeStatus() method?

Foi útil?

Solução

One of the things that the Symfony2 and FLOW3 frameworks have adopted is tagging their stable public API with an @api annotation comment.

While this is not exactly what you're looking for, it comes close. It documents the parts of your API that users can rely on. Plus your entity does not have to know about the service, avoiding the evil circular dependency.

Example:

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

Outras dicas

Use an interface which enforces the context you need:

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
  }
}

The reservation then doesn't need to know anything about the service. An alternative would be to define events on Reservation, but that's added complexity you probably don't need.

You can send messages from one domain entity to another. Only objects that are capable of producing certain messages will call the method in question. A code snippet is below. This solution is for projects where dependency injection is a sort of religion like here PHP+DDD.

The Reservation Service gets a message factory. The factory is injected through the constructor method. Objects that don't have this factory cannot issue this sort of messages. (Of course you must restrict object instantiation to factories.)

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.

    }

}

The Reservation object checks the contents of the message an decides what to do.

class Domain_Reservation
{

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

}

Message object is a DDD value object. (Sometimes it acts like a strategy.)

class Message_Item_ChangeStatusRequest
{
    private $status;

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

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

}

This factory produces messages.

class Message_Factory_ChangeStatusRequest
{

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

}

All domain objects are produced by this layer factory.

class Domain_Factory
{

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

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

}

The classes above can be used in your application as follows.

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

But I don't see why you don't want to use $reservation->beConfirmed() instead of passing status constants.

It actually sounds like this is missing a very important concept, namely a ProcessManager. A ProcessManager represents a distributed business transaction spanning multiple contexts. In reality it is a simple Finite State Machine.

An example workflow:

  1. A PlaceReservationCommand is sent to the ReservationContext, which handles it and publishes a ReservationWasPlacedEvent that the ReservationProcessManager is subscribed to.
  2. The ReservationProcessManager receives the ReservationWasPlacedEvent and validates that it can transition to the next step. I then sends a NotfiyCustomerAboutReservationCommand to the NotificationContext. It now listens to ReservationNotificationFailedEvent and ReservationNotificationSucceededEvent.
  3. Now the ReservationProcessManager sends the ConfirmReservationCommand to the ReservationContext only when it received the ReservationNotificationSucceededEvent.

The trick here is that there is no status field in Reservation. The ProcessManager is responsible of tracking the state of this business transaction. Most likely there is a ReservationProcess Aggregate that contains the statuses like ReservationProcess::INITIATED, ReservationProcess::CUSTOMER_NOTIFICATION_REQUESTED, ReservationProcess::CUSTOMER_NOTIFIED, ReservationProcess::CONFIRMATION_REQUESTED and ReservationProcess::CONFIRMED. The last state indicates a finite state that marks the process as done.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top