Practical .NET

Providing Multiple Solutions to a Problem

Sometimes life is like playing Whack-a-Mole: You write some code that solves a problem, and then someone comes along and makes the problem harder. Here's how to continuously integrate new solutions without having to rewrite your old solutions (much).

Let's start with a simple problem: You've written a class that returns Customer objects (you might call this an example of the repository pattern). That class is currently used by a single application. Inside the class you might have ADO.NET code or LINQ/Entity Framework code, but, either way, you're accessing the database -- that is to say, you're doing the two slowest things in data processing: reading the hard disk and making a call to another computer.

Your application is having some performance problems, so you're looking for ways to speed it up. You realize that, because your Customer data doesn't change very often, caching Customer objects after you retrieve them the first time and using those cached objects to satisfy later requests could speed up your application. At night, you could clear out the cache so that, when people come in tomorrow morning, they'll get the latest (and greatest!) Customer information. This is easy to implement because you can use either ASP.NET Cache object or the MemoryCache object to hold your Customer objects.

In this article, I want to show two things: First, how the Chain of Responsibility (CoR) design pattern should be used to handle this problem. Second, I want to start to show how, because of the way I design my code, I would refactor my way to that design pattern (I discussed how I design my code in an earlier article). My goal in refactoring my code is to supply "enough engineering" to support the current problem without over-engineering a solution to some later problem that might never exist. To put it another way: My solution is never more complex than the problem it's solving.

Solving Problems with the CoR Pattern
To implement your caching solution, you might be tempted to rewrite your repository to add in some caching code. Before you do that, consider two issues. The first is the Single Responsibility Principle: Your repository will now be doing two things (retrieving data from the database and managing the cache) making it more complicated, harder to debug, harder to document and so on and so forth.

The second thing to consider is that (I assume) the repository object is working right now. If you go in and add this code you'll probably introduce a bug and, when it's discovered, everyone will laugh at you. By altering the repository you're violating Vogel's first law of programming: You never screw with working code.

You'd be better off implementing the CoR pattern. The CoR pattern has the application/client call an object that tries to handle the problem. If the object can't handle the problem, the object passes the problem off to some other object that can handle the problem.

Implementing a simple version of the CoR pattern for a single client is relatively easy. Right now, the client has code like this:

Dim rep As CustomerRepository
rep = New CustomerRepository
Dim cust As Customer
cust = rep.GetById("A123")

My initial step is to replace this with code that calls the new caching class:

Dim rep As CustomerCache
rep = New CustomerCache
Dim cust As Customer
cust = rep.GetById("A123")

In the CustomerCache object, I first attempt to retrieve the Customer object from the Cache. If that fails, I call the existing CustomerRepository and ask it to get the Customer object. When the CustomerRepository does return a Customer object, I put it in the Cache to speed up retrieval the next time. Putting that all together, the code for the CustomerCache's GetById method looks like Listing 1.

Listing 1: First Draft of a Chain of Responsibility Method
Public Class CustomerCache
  Public Function GetById(custId As String) As Customer
    Dim cust As Customer
    cust = Cache(custId)
    If cust Is Nothing Then
      Dim rep As CustomerRepository
      rep = New CustomerRepository
      cust = rep.GetById(custId)
      Cache(cust.CustomerId) = cust
      Return cust
    Else
      Return cust
    End If
End Function

Notice that all you have to test here is the new CustomerCache. You have, I assume, already tested your CustomerRepository class and determined that it works. While you've given the CustomerRepository class a new client (the CustomerCache object), you haven't changed the CustomerRepository in any way. To put it another way: You've added new functionality by writing new code, not by altering existing code, so you only need to test the new code.

Things Get Worse
Unfortunately, your company now opens a new Custom Products division. Customers in the Custom division get a great deal of specialized care because their products are each uniquely crafted to meet the customer's needs.

This separation is reflected in a number of ways. The most important distinction is that the database schema for customers in the Custom Products division is very different from the current data design. Because of that, the company chooses to create two separate databases for the two divisions. You choose to create two different repositories. You also decide to flag customers in the Custom Products division by beginning their customer Ids with the letter "C."

Fortunately, because you implemented the CoR pattern, you can handle this problem by adding another item to your chain. The GetById method in CustomerRepository (which continues to handle the regular customers) can simply forward requests to the new CustomCustomerRepository when given a Custom customer. You have to, unfortunately, rewrite the GetById method in the existing CustomerRepository to check the CustomerId and forward the request appropriately:

Public Class CustomerRepository
  Public Function GetById(custId As String) As Customer
    If custId(0) <> "C" Then
      '...original code to retrieve customer object...
         Return cust
    Else
      Dim rep As CustomCustomerRepository
      rep = New CustomCustomerRepository
      Return rep.GetById(custId)
    End If
End Function

Therefore, you'll need to write two sets of tests. One set of tests will exercise the CustomCustomerRepository, the other set of tests will be added to the CustomerRepository's existing tests to prove that requests are forwarded correctly. You don't, however, have to rewrite the application -- it continues to call the CustomerCache's GetById method.

This chain can be extended indefinitely with objects that either satisfy the request or pass the request on to some object that can satisfy the request. To maintain performance, you want to make sure that the cost of determining that an object in the chain can't satisfy a request is small (it would be unfortunate, for example, if the only way for CustomerRepository to determine that it couldn't retrieve the required Customer object was to read its database). When adding a new object to the chain, the only existing code you have to rewrite is the code that calls some other object when the request can't be satisfied.

I freely admit that this design is more complicated than the original call to CustomerRepository (it certainly has more objects involved). However, I'd claim that it got more complicated only as much as the problem got more complicated … and that each individual object is still pretty simple.

I also admit that I've made some simplifying assumptions and that, as the company evolves, this design may be too simplistic. Fortunately, as I'll show in my next column, when the company's problems get more complicated, this solution can continue to evolve to handle those problems.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.