PHP 与DDD:如何确保只有Service才能调用实体上的方法?
我正在使用域模型,其中有一个 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
使用强制执行您需要的上下文的接口:
预订不需要了解有关服务的任何信息。另一种方法是在 Reservation 上定义事件,但这会增加您可能不需要的复杂性。
Use an interface which enforces the context you need:
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.
您可以将消息从一个域实体发送到另一个域实体。只有能够生成某些消息的对象才会调用相关方法。下面是一个代码片段。此解决方案适用于依赖注入是一种宗教的项目,例如这里 PHP+DDD。
预订服务获得一个消息工厂。工厂是通过构造函数方法注入的。没有此工厂的对象无法发出此类消息。 (当然,您必须将对象实例化限制为工厂。)
Reservation 对象检查消息的内容并决定要做什么。
消息对象是一个 DDD 值对象。 (有时它的作用就像一种策略。)
这个工厂生产消息。
所有领域对象都是由该层工厂生成的。
上面的类可以在您的应用程序中使用,如下所示。
但我不明白为什么您不想使用 $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.)
The Reservation object checks the contents of the message an decides what to do.
Message object is a DDD value object. (Sometimes it acts like a strategy.)
This factory produces messages.
All domain objects are produced by this layer factory.
The classes above can be used in your application as follows.
But I don't see why you don't want to use $reservation->beConfirmed() instead of passing status constants.
实际上听起来这缺少一个非常重要的概念,即 ProcessManager。 ProcessManager 代表跨越多个上下文的分布式业务事务。实际上,它是一个简单的有限状态机。
示例工作流程:
PlaceReservationCommand
发送到ReservationContext
,后者处理它并发布ReservationWasPlacedEvent
,ReservationProcessManager
已订阅。ReservationProcessManager
接收ReservationWasPlacedEvent
并验证它是否可以转换到下一步。然后,我将NotfiyCustomerAboutReservationCommand
发送到NotificationContext
。它现在侦听ReservationNotificationFailedEvent
和ReservationNotificationSucceededEvent
。ReservationProcessManager
收到ReservationNotificationSucceededEvent
时,它才会将ConfirmReservationCommand
发送到ReservationContext
。这里的技巧是预订中没有状态字段。 ProcessManager 负责跟踪此业务事务的状态。最有可能的是一个
ReservationProcess
聚合,其中包含诸如ReservationProcess::INITIATED
之类的状态,ReservationProcess::CUSTOMER_NOTIFICATION_REQUESTED
、ReservationProcess::CUSTOMER_NOTIFIED
、ReservationProcess::CONFIRMATION_REQUESTED
和ReservationProcess::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:
PlaceReservationCommand
is sent to theReservationContext
, which handles it and publishes aReservationWasPlacedEvent
that theReservationProcessManager
is subscribed to.ReservationProcessManager
receives theReservationWasPlacedEvent
and validates that it can transition to the next step. I then sends aNotfiyCustomerAboutReservationCommand
to theNotificationContext
. It now listens toReservationNotificationFailedEvent
andReservationNotificationSucceededEvent
.ReservationProcessManager
sends theConfirmReservationCommand
to theReservationContext
only when it received theReservationNotificationSucceededEvent
.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 aReservationProcess
Aggregate that contains the statuses likeReservationProcess::INITIATED
,ReservationProcess::CUSTOMER_NOTIFICATION_REQUESTED
,ReservationProcess::CUSTOMER_NOTIFIED
,ReservationProcess::CONFIRMATION_REQUESTED
andReservationProcess::CONFIRMED
. The last state indicates a finite state that marks the process as done.Symfony2 和 FLOW3 框架采用的其中一件事是使用
@api
注释注释来标记其稳定的公共 API。虽然这并不完全是您正在寻找的,但它已经很接近了。它记录了用户可以依赖的 API 部分。另外,您的实体不必了解该服务,从而避免了邪恶的循环依赖。
示例:
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: