1

I am trying to decide whether to use a Domain Service or an Entity method for a function. Please see the code below:

public class Customer
{
    private readonly Guid _id;
        private readonly decimal _expenditure;
        private readonly IList<IProduct> _eligibleProducts = new List<IProduct>();
        public IEnumerable<IProduct> EligibleProducts
        {
            get { foreach (var product in _eligibleProducts) yield return product; }
        }

    public void AddProduct(IProduct eligibleProduct)
        {
            _eligibleProducts.Add(eligibleProduct);
        }
}

The customer adds eligible products to the _eligibleProducts collection via the AddProduct method.

In order to determine what the eligible products are; I believe there are two options:

Option 1 - Domain Service

public class OfferCalculator : IOfferCalculator
{

        public IEnumerable<IProduct> CalculateEligibility(Customer customer, IList<IProduct> products)
        {
            return products.Where(x => x.IsEligible(customer));
        }
}

In this case the application service will get the eligible products from the Offer Calculator and then call Customer.Add to add the offers individually.

If I wanted to add more specific offers in future then I could have to do this (I believe):

BlackFridayOfferCalculator
ChristmasOfferCalculator
EasterOfferCalculator

I would have to add an interface on a domain service called IOfferCalculator for this.

Option 2 - Entity method

Please see the code below, which is added to the Customer class (customer class shown above):

public IEnumerable<IProduct> DetermineEligibility(IList<IProduct> availableProducts)
{
    return availableProducts.Where(x => x.IsEligible(this));
}

If I wanted to add more specific offers in future then I could have to do this (I believe):

BlackFridayCustomer
ChristmasCustomer
EasterCustomer

I would have to add an interface on an entity called ICustomer for this.

I am trying to use the principle of least astonishment.

2
  • This question is hard to understand since it depends on reading multiple external discussion threads. Is it possible to edit into a self-contained question?
    – JacquesB
    Commented Apr 6, 2018 at 9:49
  • @JacquesB, I have rewritten my question. Let me know if that helps. Thanks.
    – w0051977
    Commented Apr 6, 2018 at 10:55

1 Answer 1

2

One oracle to consider is that, while DDD advocates a clean separation between the application and the domain model, it doesn't overturn the "best practices" for writing object oriented code (where object-oriented is used in the C++/Java/C# sense).

In other words, the guideline Tell, Don't Ask still applies.

So this pattern:

In this case the application service will get the eligible products from the Offer Calculator and then call Customer.Add to add the offers individually.

fails to follow the guideline.

Observe that you are taking what is business logic (figuring out which offers are appropriate for a customer) and implementing it in an application service. Business logic belongs in the domain model.

Since Customer._eligibleProducts is the data that changes in this use case, you should be thinking that you are invoking a method on the Customer aggregate, and passing to it the data that it needs or the capability to access the data that it needs.

class Customer {
    // ...

    // note: the lousy name here is a code smell
    // TODO: consult with the domain experts to get a clearer
    // understanding of the language for this use case.

    public void updateProducts(IList<IProduct> availableProducts) {
        this._eligibleProducts.addRange(
            availableProducts.Where(x => x.IsEligible(this));
        );    
    }
}

There's a further problem to consider, which is that your IProduct.IsEligible API is asking for too much power; there's no reason, for instance, that IsEligible should be able to call AddProduct. What you should be passing instead is access to an immutable copy of the current customer state, or a read only interface.

    public void updateProducts(IList<IProduct> availableProducts) {
        this._eligibleProducts.addRange(
            availableProducts.Where(x => x.IsEligible(this.currentState()));
        );    
    }
1
  • +1 for "tell don't ask". Very helpful answer.
    – w0051977
    Commented Apr 6, 2018 at 18:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.