A Journey with Domain Driven Design (and NHibernate) - Part 9
Sunday, June 03 2007 7 Comments
Back by popular demand… (and WAY too late, I might add…)
In this article, part 9 of the series, we’re going to wrap up our initial feature list and focus on building a user-interface for our video store, named Videocracy.
Here is our feature list:
Add new Customer / AccountAdd other members to an accountRestrict Certain members from renting certain contentQuery for a customer by customer # (swipe card, etc), phone number, or last nameAdd new rental item (move game, console, vcr, etc)Rent an item to a customer- Check Items Back in
- Get movies checked out for a customer
- Query for an item, see who has it
Let’s implement Check Items Back In.
What is the use case here? Typically a customer drops off a movie, either in the slot from the outside of the store, or directly to an employee. Either case starts with the movie, or the Item.
So the employee is going to scan the item. At this time, they are going to need to see the details of who and when the item was checked out (the Rental object), and then have a confirmation button to finalize the check-in (which will update the Rental object).
In order to facilitate scanning the item, we need to be able to query the database for a particular UPC.
In implementing this next test I noticed a large problem with my domain. Our Item class defines a name property with a mapped column in the database. The problem with this is that not all items have names of their own. They might have display names that are a combination of other properties. For example, a video copy of the movie The Matrix shouldn’t have a name of ‘The Matrix’ (it really belongs to the Movie class). Typically the text on the outside of a video rental is the movie name followed by maybe the year, and the format that the movie is in. These are deferred properties, so there is no more need for a database column in the item class. I still provide a read-only Name property, and inherited classes have to provide this information. Would you have made a large change like this so eagerly in your project? If you did, how would you know if you broke some existing functionality? As I always say, it’s a good thing I have the tests to back me up…
Here’s our next test:
[Test]
public void Can_Check_In_Item()
{
Item i = TestHelper.CreateTestItem("Test_ITEM123", "1234_UPC");
Customer c = TestHelper.GetTestCustomer();
Employee empl = TestHelper.CreateTestEmployee();
using (ISession session = SessionSource.Current.GetSession())
{
using (ITransaction tx = session.BeginTransaction())
{
//store the customer and employees
session.Save(c);
session.Save(empl);
//check the item out to the customer
Rental r = new Rental(i, 2.30f);
RentalTransaction trans = c.CreateTransaction();
trans.Employee = empl;
trans.AddRental(r);
session.Save(c);
session.Save(trans);
tx.Commit();
}
}
//item is checked out, can we check it back in?
Item i2 = Repository<Item>.FindSingleByProperty("UPC", "1234_UPC");
Assert.IsNotNull(i2); //just a sanity check. Item exists in the table.
//we need to get the rental by the item upc
Rental r2 = new RentalFinder().FindByItemUPC(i2.Upc);
Assert.IsNotNull(r2, "Rental was null!");
}
There is a lot going on in this test, so let’s break it up and take a look at each part.
Item i = TestHelper.CreateTestItem("Test_ITEM123", "1234_UPC");
Customer c = TestHelper.GetTestCustomer();
Employee empl = TestHelper.CreateTestEmployee();
using (ISession session = SessionSource.Current.GetSession())
{
using (ITransaction tx = session.BeginTransaction())
{
//store the customer and employees
session.Save(c);
session.Save(empl);
. . .
Here we setup our environment. We have to have a lot of entities that already exist in order to test our new funtionality. We need an item, a customer, and employee, and an account. We create all of those and save them. (Remember all of this happens within a transaction that is rolled back, so this doesn’t stay in the database).
//check the item out to the customer
Rental r = new Rental(i, 2.30f);
RentalTransaction trans = c.CreateTransaction();
trans.Employee = empl;
trans.AddRental(r);
session.Save(c);
session.Save(trans);
tx.Commit();
Here we setup the rental and perform the rental transaction. This is how the UI will structure the business process. Once everything is saved, it’s time to verify that we can retrieve the data solely based on the item’s upc code.
//item is checked out, can we check it back in?
Item i2 = Repository<Item>.FindSingleByProperty("UPC", "1234_UPC");
Assert.IsNotNull(i2); //just a sanity check. Item exists in the table.
//we need to get the rental by the item upc
Rental r2 = new RentalFinder().FindByItemUPC(i2.Upc);
Assert.IsNotNull(r2, "Rental was null!");
The Rental Finder class will encapsultate common queries so that we can reuse them across the application. Here’s the code for that method:
public Rental FindByItemUPC(string upc)
{
Rental r = Repository<Rental>.FindSingleByQuery("from Rental r where r.Item.Upc = :upc and r.DateReturned is null", new Parameter("upc", upc));
return r;
}
There is a lot going on here in this test, but what you need to get from this is that we are setting up the stage for our scenario. As our tests get more involved we are verifying that business cases are being met. If we make drastic changes later on, we will know if we have broken existing business functionality.
The next thing to check is to make sure that we can check the item back in. I add a .Return() method on the rental, which doesn’t exist yet, so I need some additional tests.
I also need to be able to calculate late fees in a central place to make that easy to change later. I add a few tests for this as well.
It turns out that the Return() method is easy to implement. All we need to do is set the return date and calculate the late fee and save it.
[Test]
public void CanReturnItem()
{
Rental r1 = GetTestRental(DateTime.Now.AddDays(-6));
r1.Return();
Assert.IsTrue(r1.DateReturned.HasValue);
//it's 1 day late, so expect the right late fee
float lateFee = Utility.LATE_FEE_PER_DAY;
Assert.AreEqual(lateFee, r1.LateFee);
}
which leads to the following code in the Rental class…
public void Return()
{
this.DateReturned = DateTime.Now;
this.LateFee = Utility.CalculateLateFee(_dateDue, _dateReturned.Value);
}
Now we need to finish the original test and verify that we can save the rental.
r2.Return();
//save the rental
using (ISession session = SessionSource.Current.GetSession())
{
using (ITransaction tx = session.BeginTransaction())
{
session.SaveOrUpdate(r2);
tx.Commit();
}
}
This test passes and we’ve implemented our feature!
I think I’m at a point where I have demonstrated how we can work on core business features for an application test-first, using NHibernate along the way for persistence. A lot more work has to be done to complete our domain model, but that will be left as an excercise for the reader.
Instead, I would like to focus my efforts on getting a basic UI in place using ASP.NET. I said in part 1 that I wanted to demonstrate how to work with the NHibernate Session in a web environment, so that’s where I will pick up next time.
Until then, you can download and view the current project here:


Carlo Bertini
6.04.2007
1:06 PM
Many thx to be continue your tutorial (ops: my english sleep :D )