I know this is an old question, but I'd like to point out that the issue directly stems from an incorrect premise. That is, the aggregate roots we are meant to assume exist are simply incorrect.
There is only one aggregate root in the system you have described: Customer. Both an Order and Feedback, while they may be aggregates in their own right, are dependent on the Customer for existence so are not themselves aggregate roots. The logic you provide in your feedback constructor seems to indicate that an Order MUST have a customerId and Feedback MUST also be related to a Customer. This makes sense. How can an Order or Feedback not be related to a Customer? Additionally, Supplier seems to logically be related to Order (so would be inside this aggregate).
With the above in mind, all of the information you want is already available in the Customer aggregate root and it becomes clear you are enforcing your rules in the wrong place. Constructors are terrible places to enforce business rules and should be avoided at all costs. This is what it should look like (Note: I'm not going to include constructors for Customer and Order because Factories should probably be used. Also not showing all interface methods).
/*******************************\
Interfaces, explained below
\*******************************/
interface ICustomer
{
public function getId() : int;
}
interface IUser extends ICustomer
{
public function getUsername() : string;
public function getPassword() : string;
public function changeUsername( string $new ) : void;
public function resetPassword( string $new ) : void;
}
interface IReviewer extends ICustomer
{
public function provideFeedback( IOrder $order, string $content ) : void;
}
interface IBuyer extends ICustomer
{
public function placeOrder( IOrder $order ) : void;
}
interface IOrder
{
public function getCustomerId() : int;
public function addFeedback( string $content ) : void;
}
interface IFeedback
{
public function addContent( string $content ) : void;
public function isValidContent( string $content ) : void;
}
/*******************************\
Implentation
\*******************************/
class Customer implements IReviewer, IBuyer
{
protected $id;
protected $orders = [];
public function provideFeedback( IOrder $order, string $content ) : void
{
if( $order->getCustomerId() !== $this->getId() )
throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');
$order->addFeedback( $content );
}
}
class Order implements IOrder
{
protected $supplier;
protected $feedbacks = [];
public function addFeedback( string $content ) : void
{
if( false === $this->supplier->isOperating() )
throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');
// could be any IFeedback
$feedback = new Feedback( $this );
$feedback->addContent( $content );
$this->feedbacks[] = $feedback;
}
}
class Feedback implements IFeedback
{
protected $order;
protected $content;
public function __construct( IOrder $order )
{
// we don't carry our business rules in constructors
$this->order = $order;
}
public function addContent( string $content ) : void
{
if( false === $this->isValidContent($content) )
throw new \Exception("Content contains offensive language.");
$this->content = $content;
}
}
Okay. Let's break this down a little bit. The first thing you will notice is how much more declarative this model is. Everything is an action, it becomes clear WHERE business rules should apply. The design above doesn't just "do" the right thing, it "says" the right thing.
What would lead anyone to assume rules are being executed in the following line?
// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);
Second, you can see that all of the logic pertaining to validating business rules is carried out as closely as possible to the models to which they pertain. In your example, the constructor (a single method) is performing multiple validations against different models. That breaks SOLID design. Where would we add a check to make sure that the Feedback content doesn't contain bad words? Another check in the constructor? What if different kinds of Feedback need different content checks? Ugly.
Third, looking at the interfaces, you can see there are natural places to extend/modify the rules through composition. For example, different kinds of orders can have different rules regarding when feedback can be provided. Order can also provide different kinds of feedback, which in turn can have different rules for validation.
You can also see a bunch of ICustomer* interfaces. These are used to compose the Customer aggregate we need here (probably not just called Customer). The reason for this is simple. It's VERY likely that a Customer is a HUGE aggregate root that spreads out all over your domain/DB. By using interfaces, we can decompose that one aggregate (which is likely too large to load) into multiple aggregate roots that only provide certain actions (like ordering or providing feedback). You can see the aggregate in my implementation can BOTH place orders AND provide feedback, but cannot be used to reset a password or change a username.
So the answer to your question is that aggregates should validate themselves. If they can't you likely have a deficient model.
Supplier
aggregate's operating state would not be queried via anOrder
repository;Supplier
andOrder
are two separate aggregates. Secondly, there was a question on the DDD/CQRS mailing list about passing aggregate roots and repositories to other aggregate root methods (including the constructor). There was a variety of opinions, but Greg Young mentioned that passing aggregate roots as parameters is common, while another person said that repositories are more closely related to infrastructure than domain. E.g., repositories "abstract in memory collections" and have no logic.Customer
can only provide feedback on one of their own orders ($order->customerId() == $customer->customerId()
), we also have to compare the supplier ID ($order->supplierId() == $supplier->supplierId()
). The first rule guards against the user supplying incorrect values. The second rule guards against the programmer supplying incorrect values. Nevertheless, the check as to whether the supplier is operating has to be either in theFeedback
entity, or in the command handler. Where is the question.