Reputation: 148
In some business domain, I received invoicing requirements as follows:
+ Invoice Items are of two types: a service or a fee.
- Invoice Service Item is taxable. fee is not.
+ Invoice Items (services and fees) cost is calculated in two manners:
- As a whole: the service or fee has a fixed cost.
- By Individual: the cost is multiplied by Individuals count.
I’ve been reading about design patterns, domain driven design, inheritance and polymorphism, and I wanted to practice what I learnt in this side greenfield project.
I want to come to a model for the Invoicing sub domain, that captures the domain ubiquitous language. I’ve come to this model, but It doesn’t feel right!
Upvotes: 0
Views: 427
Reputation: 5017
When you model objects, try to think about it's behavior, and not only about data you need to store. Focusing on data often leads to anemic data models. If you need to calculate cost and tax, how about something like this?
internal class InvoiceItem : ValueObject
{
private readonly string _description;
private readonly decimal _cost;
private readonly int _amount;
private InvoiceItem(string description, decimal cost, int amount)
{
_description = description;
_cost = cost;
_amount = amount;
}
public decimal TotalCost()
{
return _cost * _amount;
}
public decimal Tax(ITaxCalculationPolicy taxCalculationPolicy)
{
return taxCalculationPolicy.CalculateTax(TotalCost());
}
public static InvoiceItem ByWholeInvoiceItem(string description, decimal cost)
{
return new InvoiceItem(description, cost, 1);
}
public static InvoiceItem ByIndividualInvoiceItem(string description, decimal cost, int amount)
{
return new InvoiceItem(description, cost, amount);
}
}
internal interface ITaxCalculationPolicy
{
decimal CalculateTax(decimal cost);
}
internal class ServiceTaxPolicy : ITaxCalculationPolicy
{
private const decimal TaxPercent = 0.18m;
public decimal CalculateTax(decimal cost)
{
return cost * TaxPercent;
}
}
internal class FeeTaxPolicy : ITaxCalculationPolicy
{
private const decimal TaxValue = 0;
public decimal CalculateTax(decimal cost)
{
return TaxValue;
}
}
If you don't want a client to decide what kind of ITaxCalculationPolicy
should be used, you could also try with passing some kind of ITaxCalculationPolicyFactory
to Tax()
method. Than you would have to store a type of Invoice Item inside it and pass it to that factory when calculating Tax.
Upvotes: 1
Reputation: 13256
My recommendation would be to avoid inheritance in domain models as much as possible. You may come across a use-case for inheritance every-so-often but it would probably relate more to technical implementation details than the UL.
I also avoid designing classification as structure since it makes you model less flexible. Structure should reflect the "fixed" bits that really are not going to change such as Invoice
/InvoiceItem
. A type of invoice item seems to fit as a classification. These things can be tricky to spot but when you have a class explosion it may be a red flag. As soon as domain experts start breaking down concepts into "types" then you are probably looking at a classification structure of sorts.
Also, avoid injecting services into domain objects. You may opt for double-dispatch where a service, that implements a service interface, is injected into the relevant method and the method calls the service to obtain, say, the relevant tax. Another way, that I prefer, is to rather pass in the value(s) required by the method.
Keep in mind that there are going to be technical implementation details that are not going to be part of the UL so don't worry about having each part of the UL represented as a class. As long as you have the object behaviour and shape of the UL captured in some form, and the UL can be completely represented, then you shouldn't have any issues.
Upvotes: 2