PHP 与DDD:如何确保只有Service才能调用实体上的方法?

发布于 2024-12-14 01:47:19 字数 1100 浏览 0 评论 0原文

我正在使用域模型,其中有一个 Reservation 类:

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() 方法的访问吗?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

猫瑾少女 2024-12-21 01:47:19

使用强制执行您需要的上下文的接口:

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 上定义事件,但这会增加您可能不需要的复杂性。

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.

芸娘子的小脾气 2024-12-21 01:47:19

您可以将消息从一个域实体发送到另一个域实体。只有能够生成某些消息的对象才会调用相关方法。下面是一个代码片段。此解决方案适用于依赖注入是一种宗教的项目,例如这里 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);

但我不明白为什么您不想使用 $reservation->beConfirmed() 而不是传递状态常量。

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.

夏雨凉 2024-12-21 01:47:19

实际上听起来这缺少一个非常重要的概念,即 ProcessManager。 ProcessManager 代表跨越多个上下文的分布式业务事务。实际上,它是一个简单的有限状态机

示例工作流程:

  1. PlaceReservationCommand 发送到 ReservationContext,后者处理它并发布 ReservationWasPlacedEventReservationProcessManager已订阅。
  2. ReservationProcessManager 接收 ReservationWasPlacedEvent 并验证它是否可以转换到下一步。然后,我将 NotfiyCustomerAboutReservationCommand 发送到 NotificationContext。它现在侦听 ReservationNotificationFailedEventReservationNotificationSucceededEvent
  3. 现在,仅当 ReservationProcessManager 收到 ReservationNotificationSucceededEvent 时,它才会将 ConfirmReservationCommand 发送到 ReservationContext

这里的技巧是预订中没有状态字段ProcessManager 负责跟踪此业务事务的状态。最有可能的是一个 ReservationProcess 聚合,其中包含诸如 ReservationProcess::INITIATED 之类的状态,
ReservationProcess::CUSTOMER_NOTIFICATION_REQUESTEDReservationProcess::CUSTOMER_NOTIFIEDReservationProcess::CONFIRMATION_REQUESTEDReservationProcess::CONFIRMED。最后一个状态表示有限状态,将流程标记为完成

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.

狂之美人 2024-12-21 01:47:19

Symfony2 和 FLOW3 框架采用的其中一件事是使用 @api 注释注释来标记其稳定的公共 API。

虽然这并不完全是您正在寻找的,但它已经很接近了。它记录了用户可以依赖的 API 部分。另外,您的实体不必了解该服务,从而避免了邪恶的循环依赖。

示例:

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

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;
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文