webdevilopers / php-ddd

PHP Symfony Doctrine Domain-driven Design
201 stars 10 forks source link

Finding aggregate (root) and naming (factory) methods #1

Open webdevilopers opened 8 years ago

webdevilopers commented 8 years ago

What really interests me is defining the actual aggregate root from the Order POV.

In my current company I have to processes:

Regarding the Offer and Order it is the Employee with the active part. In the red book there is this example "Table 1.5 Analyzing the best model for the business.": http://www.informit.com/articles/article.aspx?p=1944876&seqNum=3

Now I started thinking that it should be something like:

or at least:

instead of:

This really confused me! ;) My feeling as a developer tells me to use the last approach. What do you think @yvoyer ?

yvoyer commented 8 years ago

I created an example of how I would do your requirements. They are not working example, just code fragment from which you can get a basic idea.

Here are what classes or methods I extrapolated from your statements:

Customer talks to Employee. Employee logs in, creates an offer, sends it to Customer

  • I created an Employee->createOffer(): Offer that is handled by a command handle CreateOfferHandler which could be called from some controller.
  • The event listener SendOfferToCustomer::onOfferWasCreated(OfferWasCreated $event) manage the sending of the offer to the customer.

Customer receives Offer, agrees, informs Employee

  • The command handler AgreeToOfferHandler calls the Customer->agreeToOffer(OfferId) which change to state of the offer with Offer->agree().
  • An event CustomerAgreedToOffer event is triggered, and received by the OfferAgreedNotifier listener. From there you can do whatever there is to do to notify the employee.

I did not completed all the requirements, but I hope this gives you a better idea.

Now I started thinking that it should be something like:

Employee->sendOfferToCustomer Employee->sendOrderToCustomer Customer->confirmOffer

or at least:

Customer->receiveOffer Customer->receiveOrder Customer->confirmOffer

IMO, The Customer should not know about the receiveOrder, receiveOffer, and the Employee should not know about the sendOfferToCustomer or sendOrderToCustomer since Customer and Employee seems to be 2 aggregates roots from 2 differents context. It is ok for them to receive Id, but the aggregate objects should probably not be passed to this context from any other places than the handlers.

But then again, I don't know all the requirements or needs, so it all depends on your domain.

Good luck, and hope it help.

webdevilopers commented 8 years ago

Great feedback @yvoyer ... again.

So looking at Employee and Customer from a different Context really changes the responsibility of the actor on the Order Domain Model? Indirectely the Customer is "placing an Order" when calling. But it is the Employee who adds it to the system.

Surprising and exciting at the same time.

BTW: Do you recommend using the Symfony Event Listeners or have you tried the @SimpleBus EventBus? https://github.com/SimpleBus/EventBus

P.S. on terminilogy:

Regarding: https://www.ego4u.com/en/business-english/communication/offer-order

BTW: Really like the $offer = self::PendingOffer($responsible); approach!

yvoyer commented 8 years ago

I have not tried SimpleBus yet, but it seems simple. I heard good review about it though.

I have used lite-cqrs for commands and EventDispatcher from symfony.

The only downside of the symfony dispatcher is having to extend the Event class. Try the lib that you find will help you accomplish your goal sooner.

About the terminology, I would probably go with terms like you said submitOffer and placeOrder as they are probably more meaningfull to the domain users. You should probably ask your domain expert to see which terminology is better for them.

BTW: Really like the $offer = self::PendingOffer($responsible); approach!

My pleasure, This is a good example of factory method. Since PHP do not enable multiple construct definitions, the static methods acts as constructs. I would also recommend to put your __construct private to force the dev (an you) to use the static methods instead of the construct. Makes the code more meaningfull.

For the State vs Status it a mather of choice on your part.

webdevilopers commented 8 years ago

And again we agree @yvoyer ! I will try to adapt what we talked about to my current project. There will be feedback by the Customer and the Developers at the beginning of next month. I will post some approved code samples then! Thanks so far!

webdevilopers commented 8 years ago

One question regarding your getOffer method: https://gist.github.com/yvoyer/c2c016b7fe5288554e3f6c432acafd65#L124-131

Though it is the Customer that indirectly confirms an Offer it is still the Employee who uploads an Order Confirmation and by this marks it as confirmed.

So I guess I have to move this method to the Employee too.

If I moved the getOffer method there would the Employee have an $offers collection I would use like this?

class Employee
{
    $offers = array(); // some extra lazy Doctrine ArrayCollection possibly

    private function getOffer(OfferId $id)
    {
        return $this->offers[$id];
    }
}

While it would make sense to add the same $offers collection on the Customer the getOffer method should be called on the Employee side, right @yvoyer ?

yvoyer commented 8 years ago

For me it still makes sense for the customer to have a getOrder (OrderId):Order. Since it is private, it do not bloth the domain, but it is still usefull to fetch 1 order. If you need it also in the employee class, it's okay, but I would keep it private as long as possible.

Try not to get the object B of another object A to perform an operation. Just tell object A to perform operation on B, that way A keeps control, and any validation constraint are encapsulated in A.

side note

Employee logs in - an actual "Contract" is created. Work can begin

From this sentence I would probably create a Employee::getApprovedContract (ContractId): Contract. When the Customer would confirm its Order, the system would automatically create the Contract and assign it to the employee and customer using events.

webdevilopers commented 8 years ago

That brings me back to the beginning of this discussion.

As I mentioned it isn't the Customer in person placing the Order. He calles the Office and the Employee enters it into the system. Later when the Office receives the Order Confirmation it uploads it.

Maybe Office / Employee are just Roles inside the System. Maybe placing and Confirming the Order should ONLY happen in the Customer Domain Model.

There is no urgent need to find out which Employee created which Order. In the end the Employee is just a placedBy or confirmedBy link to the User behind Employee.

Is this still okay from a DDD and Context view?

yvoyer commented 8 years ago

If the customer never touches the system, I would definitely put the task to create the order on the employee. You would still need the customer ID to create the order (but not the whole customer).

Maybe instead of an Employee you need a new domain entity named SalesRepresentative or any other name meaningful for your domain expert. As an employee might be too generic, as it depends upon the bounded context. In the human ressource context, it might make sense to have an Employee. Where in a sale context, you might have the SaleRepresentative which is in charge of taking order from customer.

Then the order would be shipped for confirmation to the shipping context where the order would be prepared by a Shipper to the buyer (customer with address info).

Finally when the order is shipped it might be sent to the quality assurance context to be handled by the AfterSaleRepresentative to take any complaints.

Maybe what is making your domain ambiguous is that you might need to explode it in context and see which finner grained entity is missing. You probably will find with your domain expert that some other terms are missing.

webdevilopers commented 8 years ago

Actually we do have something like a SalesRepresentative. The Employee belongs to the Office. Though Office simply extends Employee atm. And the Office will have to confirm the Order after having placed it.

I'm wondering how the Office will receive the Offer to change it.

In both cases an $order has to be returned to be updated in the repository.

You see I still have problems with "ignoring the persistence". :/