Introduction
This is the 3rd post of a serie of posts on Domain Driven Design and Test Driven Development.
In this article, I'll continue from where the previous article ended. This means that I'll continue with the implementation of the Domain Model that was defined in the first article of this serie.
In the previous article, we tackled the functionality of Customers placing Orders, and Gold Customers receiving a discount.
In this article, I'll focus on the behaviour for Bad Paying Customers and the creation of Invoices.
Bad Paying Customers
Now that we can determine whether a Customer is a 'Gold' Customer or not, we should build the functionality to determine whether a Customer is a bad payer or not.
Just like I've done for the 'Gold Customer' case, I'd like to kick off with writing a small unit-test.
A Customer is tagged as a bad paying Customer when 1/3 of his invoices are overdue/payed too late. The consequence of being a bad paying Customer is, that such a Customer cannot place Orders whose OrderTotal exceeds 250 euro. The test looks like this:
public void TestBadPayingCustomerPlacesOrder()
{
Customer c = custRep.GetCustomer (3);
// We 'know' that Customer 3 is a bad paying Customer.
// To check this, we can check the
// Status of the Customer.
Assert.AreEqual (CustomerStatus.BadPayer,
c.Status,
"This should be a bad paying Customer.");
// Get the Articles that we'll need.
Article art1 = artRep.GetArticle (1);
Article art2 = artRep.GetArticle (2);
// Check the prices of the articles, so that we
// do not make any false assumptiosn here.
Assert.AreEqual (15M, art1.Price,
"The 1st Article should have a unit-price of 15eur.");
Assert.AreEqual (12.5M, art2.Price,
"The 2nd Article should have a unit-price of 12.5eur.");
// Create the Order...
Order o = c.CreateNewOrder();
// and add the order-lines.
OrderLine ol1 = o.CreateNewOrderLine();
ol1.SetArticle (art1);
ol1.NumberOfItems = 10;
OrderLineAddQueryResult result = o.CanOrderLineBeAdded (ol1);
Assert.AreEqual (OrderLineAddQueryResult.Yes,
result,
"The 1st OrderLine should pose no problem.");
// At this time, the OrderTotal should be 150eur.
Assert.AreEqual (150M,
o.TotalAmountToPay,
"The OrderTotal should be 150eur.");
// Create a 2nd OrderLine which will cause
// the Order-limit to be exceeded.
OrderLine ol2 = o.CreateNewOrderLine();
ol2.SetArticle (art2);
ol2.NumberOfItems = 10;
result = o.CanOrderLineBeAdded (ol2);
Assert.AreEqual (
OrderLineAddQueryResult.NoBecauseOrderLimitExceeded,
result,
"Bad Paying Customers are not allowed to place orders > 250eur.");
}
Before we can make this test succeed, we must be able to compile it. This means that we'll have to extend the OrderLineAddQueryResult
enumeration with the NoBecauseOrderLimitExceeded
value. This is a simple task, and just a matter of adding this value to the enumeration type. This is in fact so simple, that I'm not going to elaborate on that here.
Once this is done, the test compiles, but it fails. This is due to the fact that we haven't implemented any functionality yet to determine whether a Customer is a bad paying Customer or not, so lets do that right away.
A Bad Paying Customer is a Customer that has at least one out of three overdue invoices. To be able to check this, we need to introduce a new entity: the Invoice.
Invoices are made for Orders that are shipped. An invoice contains a date when the Invoice has been created, and a due-date. Offcourse, it also contains the list of Articles that have been ordered and where the Customer has to pay for, and, it contains the amount that the Customer has to pay.
Since Invoices
are created from Order
s that have been shipped, we can start with this unit-test:
[Test]
public void CreateInvoice()
{
Order o = orderRepository.GetOrder (2);
Invoice inv = o.CreateInvoice();
}
This is rather simple! But simple is good.
As we can learn from the test, we'll need an Invoice
entity, and we'll need to write a CreateInvoice
method in the Order
class.
Let's start off with the new entity. The Invoice class contains these members:
public class Invoice
{
private int id;
private DateTime invoiceDate;
private DateTime dueDate;
private Customer owningCustomer;
private InvoiceStatus status;
private bool isOverdue;
private DateTime datePayed;
private decimal subTotal;
private decimal discount;
private IList invoiceLines = new ArrayList();
public int Id
{
get
{
return id;
}
}
public DateTime InvoiceDate
{
get
{
return invoiceDate;
}
}
public DateTime DueDate
{
get
{
return dueDate;
}
set
{
dueDate = value;
}
}
public Customer OwningCustomer
{
get
{
return owningCustomer;
}
}
public InvoiceStatus Status
{
get
{
return status;
}
}
public bool IsOverdue
{
get
{
return isOverdue;
}
internal set
{
isOverdue = value;
}
}
public DateTime DatePayed
{
get
{
return datePayed;
}
}
public decimal SubTotal
{
get
{
return subTotal;
}
}
public decimal Discount
{
get
{
return discount;
}
}
}
An Invoice
should not be created directly by the user of our Domain classes, so it's not necessary (and not favorable) to have a public constructor in this class. Therefore, I've decided to create an internal constructor. An Invoice
is also created for one Order
, so the constructor can take the Order
for which the invoice is created, as an argument. Then, the constructor and it's implementation looks like this:
public class Invoice
{
...
internal Invoice( Order o, DateTime invoiceDate, DateTime dueDate )
{
id = -1;
this.invoiceDate = invoiceDate;
this.dueDate = dueDate;
owningCustomer = o.OwningCustomer;
status = InvoiceStatus.Open;
isOverdue = false;
subTotal = o.OrderTotal;
discount = o.Discount;
foreach( OrderLine ol in o.OrderLines )
{
AddInvoiceLine (ol.ArticleId,
ol.ArticleName,
ol.ArticlePrice,
ol.NumberOfItems);
}
}
}
The AddInvoiceLine
method will add an InvoiceLine
object to the invoiceLines
collection for each OrderLine
object that is contained by the Order
:
private void AddInvoiceLine( int articleId,
string articleName,
decimal articlePrice,
int numberOfItems )
{
invoiceLines.Add (
new InvoiceLine (this, articleId,
articleName, articlePrice, numberOfItems));
}
An InvoiceLine
object contains information about the article for which the Customer
has to pay, and, offcourse, it contains a pointer to the Invoice
to which it belongs.
The CreateInvoice
method should also be implemented. This is quite simple; in this method, we'll just use the internal constructor of the Invoice
class, and we can also check if the Order
has been shipped, since it is not allowed to create Invoices
for Orders
that haven't been shipped.
public class Order
{
...
public Invoice CreateInvoice()
{
if( this.Status != OrderStatus.Shipped )
{
throw new ApplicationException ("The order hasn't been shipped yet.");
}
return new Invoice (this, DateTime.Now, DateTime.Now.AddDays (30));
}
}
As you can see, the dueDate
for the Invoice
is calculated in this method. By default, the Customer
has 30 days time to pay his invoice.
Since this logic is quite simple, it can safely be placed inside the CreateInvoice
method. If the calculation of the due date would becomes more complicated, we could opt to factor this logic into a specification object.
Once the
InvoiceLine
class is implemented and the CreateInvoice
method has been written, the tests compile. However, the first test still fails, because we still have no functionality written to determine if a Customer
is a bad payer.To do this, we can extend the
Status
property of the Customer
class.public CustomerStatus Status
{
get
{
CustomerStatus result = CustomerStatus.Normal;
// First get the amount of orders this
// customer has made in the last 3 months.
decimal orderTotal = DomainSettings.Instance.
RepositoryFactoryObj.
CreateOrderRepository().
GetOrderTotalForCustomerSinceDate (this,
(DateTime.Now.AddMonths (-3));
if( orderTotal > DomainSettings.GoldAmountTreshold )
{
result = CustomerStatus.Gold;
}
// Check if the Customer is a bad Payer,
// if the Customer is a Gold Customer AND a bad payer,
// the Status should be 'Bad Payer'.
// Get the number of invoices for this customer,
// and get the number of overdue invoices.
IInvoiceRepository invRep =
DomainSettings.Instance.
RepositoryFactoryObj.
CreateInvoiceRepository();
int numberOfOverdueInvoices =
invRep.GetNumberOfOverdueInvoicesForCustomer (this);
if( numberOfOverdueInvoices > 0 )
{
int totalInvoices = invRep.GetNumberOfInvoicesForCustomer (this);
if( numberOfOverdueInvoices / totalInvoices > 0.333 )
{
result = CustomerStatus.BadPayer;
}
}
return result;
}
}
In order to get this code to compile, the IInvoiceRepository
interface and an implementation of it have to be created:
public interface IInvoiceRepository
{
int GetNumberOfOverdueInvoicesForCustomer( Customer c );
int GetNumberOfInvoicesForCustomer( Customer c );
}
I'll also create an InvoiceMemoryStoreRepository
for the test-cases in the project that contains the unit-tests, and I'll extend the IRepositoryFactory
interface (and the classes that implement this interface) with a method that creates the correct IInvoiceRepository
.
Once that is done, the CanOrderLineBeAdded
member method of the Order
class needs to be extended, so that we check if a Customer
is allowed to add the OrderLine
to it's Order
. This means that we'll have to adapt the Order
class a bit.
First of all, I'd like to change the isGoldCustomer
boolean member that acted as some kind of a helper variable, in order that we do not have to check the Status
property of the OwningCustomer
(because checking this status can be considered as expensive).
I want to do that because it is not sufficient any more to know if the 'owning customer' of the order, is a Gold Customer or not; now, I want to know whether the customer is a Normal Customer, a Gold Customer or a Bad Payer. Therefore, I removed the isGoldCustomer member variable, and changed it to a customerStatusHelper member, that is of type CustomerStatus
.
I've put the code that has to keep track of the CustomerStatus
into a separate method that looks like this:
private void DetermineCustomerStatus()
{
customerStatusHelper = owningCustomer.Status;
customerStatusDetermined = true;
}
Then, the CalculateDiscount() member method of the Order class has to be changed as well:
private void CalculateDiscount()
{
if( customerStatusDetermined == false )
{
DetermineCustomerStatus();
}
if( customerStatusHelper == CustomerStatus.Gold )
{
_discount = OrderTotal * 5 / 100;
}
else
{
_discount = 0M;
}
}
Although this is a rather small refactoring, thanks to our unit-test, we can be assured that our code is still correct.
Now, we can add a test to the CanOrderLineBeAdded
member method to check whether the 'OrderLimit' is exceeded, when a Customer
who's a Bad Payer, makes an Order
:
public OrderLineAddQueryResult CanOrderLineBeAdded( OrderLine ol )
{
OrderLineAddQueryResult result = OrderLineAddQueryResult.Yes;
if( customerStatusDetermined == false )
{
DetermineCustomerStatus();
}
if( customerStatusHelper == CustomerStatus.BadPayer )
{
if( this.TotalAmountToPay + ol.LineTotal >
DomainSettings.BadPayerOrderLimit )
{
result = OrderLineAddQueryResult.NoBecauseOrderLimitExceeded;
}
}
// the other checks that were shown before, are now not shown any more.
...
...
return result;
}
The GetReasonWhyOrderLineCantBeAdded needs to be extended as well, but I'm not going to discuss that.
Creating Invoices
Until this time, we're still unable to create Invoices
for Orders
that are shipped. As discussed earlier, creating Invoices
from shipped Orders
would best be implemented as a Service.
This 'Domain Service' can then be used by a Windows Service, so that this task is done every night. The CreateInvoices
service should use an IOrderRepository
to get all the Orders
that are shipped, and for which no Invoice
has been created yet.
For those Orders
, the service should create Invoices
. Given the text, the service should look like this:
public static class CreateInvoicesService
{
public static void CreateInvoices()
{
IOrderRepository ordersRep =
DomainSettings.RepositoryFactoryObj.CreateOrderRepository();
IInvoiceRepository invoiceRep =
DomainSettings.RepositoryFactoryObj.CreateInvoiceRepository();
IList orders = ordersRep.FindShippedOrdersWithoutInvoice();
foreach( Order o in orders )
{
Invoice inv = o.CreateInvoice();
invoiceRep.Save (inv);
}
}
}
This is pretty straightforward. However, there is something that we haven't considered yet: transaction handling/boundaries.
The Repository itself should not be responsible for starting and committing or rollbacking transactions.
The Repository is not aware of the context of the transaction, so therefore, it is not the task of the repository to start and commit a transaction. Only the client knows when to start and commit a transaction, so it's his responsability. In this case, it's the CreateInvoicesService
service that is responsible for starting and committing (or rollbacking) the transaction.
But since we haven't focused on persistence yet, we'll just keep this in the back of our mind.
Now that we're able to create Invoices
, we're still not able to determine whether an Invoice
is overdue or not. This batch-process should also be implemented as a service.
An IInvoiceRepository
should be used that gives us all Invoices
that haven't been payed yet.
Then, each Invoice
that has been returned can be checked by a Specification class if the Invoice
is overdue or not. If the Invoice
is overdue, the IsOverdue
flag can be set, and the Invoice
can be persisted. If the logic to determine whether the Invoice
is overdue or not would be complicated, and depend on several other factors (like, for instance the CustomerStatus
of the Customer
), then this would be a good solution.
In this case however, things are quite simple: an Invoice
is overdue if it is past its duedate (however, we could allow a retention period of say 4 days). This means that marking all Invoices
that are past their due date as overdue, could be done be just one query. In other words, the MarkOverdueInvoices Service could be as simple as just calling a method on the IInvoiceRepository
:
public static class MarkOverdueInvoicesService
{
public static void MarkOverdueInvoices()
{
IInvoiceRepository invRep =
DomainSettings.RepositoryFactoryObj.CreateInvoiceRepository();
invRep.MarkOverdueInvoices();
}
}
So, I think that's about it. I think we've discussed the most important part of the Domain. Now, we can concentrate on the persistance of the objects in a relation database. For more information about test driven development, you can read these guidelines, written by Jeffrey Palermo.
That's it for now. In the next article (if it ever comes, since I still have to start writing...) of this series, I'll try to tackle the persistence functionality using NHibernate.
3 opmerkingen:
Hi Frederik,
Good article, i'm enjoy reading your explanation on DDD and TDD.
I already ask this question in the part 1 of this series, but i'm sorry didn't update back to you with UML design and database design.
It's very simple scenario..where in my application i have a function to create a set of Question base on selected Topic.
But in my case i design the Topic object as aggregate boundry of Subject meaning Subject act as aggregate root . So if i follow DDD rules Question cannot have a reference to a Topic but Question must get it from Subject.
In DDD rules an outside entity cannot have reference to internal aggregate boundry but only have a reference to root aggregate, how do u think to design this model, how i can have access to Topic?
How to design this?..Is it ok to design the class something like this?But here the Question still have reference to the Topic?
public class Question()
{
private Topic _topic;
public Question(Subject subject)
{
_topic = subject.SelectedTopic;
}
}
Question.gif
QuestionDB.gif
Ok, so you have a Topic which contains several Questions.
Then, you're talking about Subject. How does this relate ?
A Subject contains several topics ?
With my little knowledge (none) of your problem domain, I think you could say that Subject is an aggregate, and you have another aggregate that contains the classes Topic and Question, where Topic is the aggregate boundary.
Yes subject contains several Topics.
I'm not sure is it suitable for Question to be aggregate boundry in Topic, what do u think?The problem for having nested aggregate is on performance.
Een reactie posten