Sunday, July 23, 2006

A Journey with Domain Driven Design (and NHibernate) - Part 4

Welcome back!  I’ve gone and switched the title of these articles all together to reflect the content more adequately!  (Maybe I should have written the series ahead of time to avoid such issues, ya think?)

Anyway, last time we introduced some very basic unit tests.  We simply verified that we can indeed create each object and pass in the invariants as constructor parameters.  The only real decision making that this influenced was how to create objects.  For example, it is no problem at all to allow direct creation (public constructor) on root objects such as Movie, Video, Account, and Customer, however I chose not to let Transaction be created directly, rather it is created from an existing Account object.  The reason for this is simple.  Picture what happens at a video store:  You go to the counter with your card and your videos, the employee scans the card and sees your account details.  The next thing that the employee will do is start a new transaction.  An account object is always present at the time that we need to get a transaction object.  Another reason for this is I decided to satisfy the invariants of the transaction (specifically the account) at creation time.

Jimmy Nilsson talks about creating fluent interfaces in his book Applying Domain Driven Design.  We want our object model to express intent and reinforce correct usage.  To demonstrate this, consider the following code two segments:

1)  Transaction tx = new Transaction(_account);
2)  Transaction tx = _account.CreateTransaction();

I think that the 2nd option conveys more meaning and is more fluent than the first.  This is, however, an early design choice and is always up to change with further refactorings.

I left off last time with a list of passing tests.  (Sometimes people suggest to leave a failing test so you know right where to pick up the next day.)  I have implemented a few more trivial tests, which I won’t bore you with here.  Let’s get on with the more interesting tests.  Another thing I’d like to note is that, the tests here pass, but only after I write the appropriate code in the model to satisfy the test.  I am purposefully omitting the domain model code at this time because it is unimportant.  You should be able to understand what the code does without looking inside the class.  Onward….

As mentioned in the 1st article, a transaction will have many rentals.  That is because you can rent more than 1 video per visit.  The price of renting movies will vary, so in order to have history of what price was paid for an old transaction, we must allow the Rental to carry a price.  Another thing we will have to consider is how to determine when an item is due.  We’ll have to communicate with some service that will tell us when an item is due, given whether it is a new release, or a hardware item, etc.  All we are concerned about at this time is that the rental knows it has a due date.  How the due date is calculated is a responsibility outside of the Rental class.  Agreed?  On with the tests.

[Test]

public void CanCreateTransactionForAccount()

{

    Account a = new Account();

    Customer c = new Customer("john", "doe");

 

    Transaction tx = a.CreateTransaction();

    tx.Customer = c; 

 

    //we can only be accurate up to the millisecond on the date here, but we can create a timespan of 2 hours just

    //to be sure.

    Assert.IsTrue(tx.TransactionDate.CompareTo(DateTime.Now.AddHours(-1)) > 0 &&

                    tx.TransactionDate.CompareTo(DateTime.Now.AddHours(1)) < 0, "Date wasn't set");

 

    Assert.IsNotNull(tx, "Transaction wasn't returned");

    Assert.AreEqual(a, tx.Account, "Account wasn't set");

 

    Assert.IsNotNull(tx.Rentals, "Rentals collection was null");

    Assert.AreEqual(0, tx.Rentals.Count, "Rentals collection wasn't empty");

 

    Assert.AreEqual(0.0, tx.SubTotal);

}

This test is a bit longer than previous ones, so I’ll go over it.  We start by creating a customer and account to use in our test.  Then we use the account object to create a transaction.  The next sets the Customer that is actually renting the item.  We need this because we may prevent the customer from renting items above his/her rental restriction.  I’m now checking to see if the date was set.  I used a simple 2 hour date range, because I cannot guarantee how fast this code will run.  The idea is that the date and time will change from MinValue to Now after the creation of the Transaction.  This will be caught by the assertion.  The rest of the Asserts should be self-explanatory.

Now we are able to create a transaction, we should now be able to create rentals and add them to the transaction.  Adding the rental to the transaction should change the transaction’s subtotal properly, so we need to test that as well, however this should be broken up into two tests (Remember, try to only test one unit at a time!).

[Test]

public void CanAddRentalToTransaction()

{

    Transaction tx = _account.CreateTransaction();

    tx.Customer = _customer;

 

    Rental r = new Rental(_video, 3.50f);

    tx.Rentals.Add(r);

 

    Assert.AreEqual(1, tx.Rentals.Count, "Rental wasn't added to transaction");

    Assert.IsTrue(tx.Rentals.Contains(r), "Rental wasn't the same!");

 

    Rental r2 = new Rental(_video2, 3.50f);

 

    tx.Rentals.Add(r2);

 

    Assert.AreEqual(2, tx.Rentals.Count, "2nd rental wasn't added to transaction");

    Assert.IsTrue(tx.Rentals.Contains(r2), "2nd rental wasn't the same!");

}

As you can see, we create a rental, add it to the transaction, and verify that the collection count is correct and contains the new rental.  We do it twice just to be sure.

One concept I haven’t shown yet is to test invalid usage of the model.  We need to do this to make sure our domain model behaves accordingly if it is abused.  Here’s a good example:

[Test]

public void CanNotAddSameRentalToTransactionTwice()

{

    Transaction tx = _account.CreateTransaction();

    tx.Customer = _customer;

 

    Rental r = new Rental(_video, 3.50f);

    tx.Rentals.Add(r);

    tx.Rentals.Add(r);

 

    Assert.AreEqual(1, tx.Rentals.Count, "Rental was added twice!");

}

There is no conceptual reason that a rental should be added twice, so we check to make sure this isn’t allowed.

Up next, we need to verify that the Transaction’s subtotal property is calculated correctly.

[Test]

public void TransactionSubTotalGiveSumOfRentalPrices()

{                     

    Transaction tx = _account.CreateTransaction();

 

    Assert.AreEqual(0.0, tx.SubTotal, "No rentals should yield 0.00 subtotal");

 

    tx.Rentals.Add(new Rental(_video, 3.50f));

    Assert.AreEqual(3.50, tx.SubTotal, "SubTotal should be 3.50 after adding a 3.50 rental");

 

    tx.Rentals.Add(new Rental(_video2, 3.50f));

    Assert.AreEqual(7.00, tx.SubTotal, "SubTotal should be 7.00 after adding 2 3.50 rentals");

}

This test failed initially, and to get it to pass I had 2 choices of implementation:  have a dynamic property that calculates the sum of the rental prices on the fly, or capture the collection adding/removing actions and adjust it appropriately.  I chose the latter option, because I would like to persist this as a de-normalized column in the database.  This way I can deal with a transaction object without actually loading up all the rentals along with it.  You’ll see the benefit of this a bit later when we talk about NHibernate’s lazy loading.

Here’s  the (slightly modified) feature list.  I’ve colored the already implemented features in green.

  • Add new Customer / Account
  • Add other members to an account
  • Restrict certain members from renting certain content
  • Query for a Customer by Customer # (swipe card, etc), phone number, or last name
  • Add new rental item (movie, video game, game console, vcr, etc)
  • Rent an item to a customer
    • Create a Transaction
    • Add Rental To Transaction
    • Finalize Transaction
    • Calculate due date for an item
  • Check items back in from a customer
  • Get movies checked out for a customer
  • Calculate late fees for a customer
  • Query for an item, see who has it

We’re coming to a point where we need to start persisting our domain model.  Most of the remaining  features to implement will employ querying a database, so we’ll probably start on persistence next time.

Tuesday, August 15, 2006 11:42:06 AM (Central Standard Time, UTC-06:00)
I'm looking forward to getting Jimmy's book, it's in my Amazon.co.uk basket ready to be bought! I'm a secret admirer of DDD and love the way it promotes readable code that reflects the intent of the domain.

Looking at your example, I think that you're right in that the factory method for account makes more sense. I initally thought that you were talking about database transactions, so you could consider renaming Transaction to "Rental Transaction" or similar? That said, if the word "Transaction" makes sense to the stakeholders then is it wrong to change it?

Does a transaction always need a customer, and does an account always have one customer? If so you could either do...


Transaction t = _account.CreateTransaction();
Assert.AreEqual( _customer, t.Customer );

or

Transaction t = _account.CreateTransaction(_customer);
Assert.AreEqual( _customer, t.Customer );

When it comes to adding Rentals to a Transaction, you could always consider using a Set/Dictionary under the hood for to enforce that the same Rental is not added twice. NHibernate has types for this as far as I recall. This would result in an exception being thrown on the 2nd call. Some folks don't like seeing exceptions for business rule violations, what's ur take on this?

In contrast, if using RubyOnRails, such checks would usually be peformed very late in the game, when a tx.Isvalid is called, or a tx.Save() is called.

There's loads of options for all this kind of thing and it's tricky to know the best approach, it will be interesting to see how your solution pans out!
Tuesday, August 15, 2006 11:52:28 AM (Central Standard Time, UTC-06:00)
What you describe is something that I have fought with back and forth.



As for Transaction naming, I did originally cringed at this because there is the concept of a transaction in the database and in the DAL (NHibernate), so it may be of benefit to change it to RentalTransaction (and I'm unsure of why I chose to stick with it in the first place). This is a model change so it should involve the business users... but we have to remember that the "Ubiquitous Language" is not just the current shop-talk. Business users can and will adapt to use the language of the domain as will developers. RentalTransaction will makes sense to both parties, so I will consider this change as I complete part 5 of the series. Refactoring IS a key element to good design, so it might prove beneficial to go over that change. I should be able to:
-Question everything.
-Rigorously refactor.
-Have unit tests confirm changes worked
Tuesday, December 05, 2006 5:37:44 PM (Central Standard Time, UTC-06:00)
I am very much enjoying your series thus far (trying to read it one day at a time). I myself recently purchased Jimmy's book and am about half way through it. In addition I am now using TDD in all of my projects as they are a godsend.

One question I have is why you aren't mocking out some of the classes such as customer or other classes that are beyond the scope of the unit tests you are describing. I am assuming you are doing this to keep the complexity of the series minimal?
Comments are closed.
Loans - Credit Card - Arizona Pools - Vegas Hotel