Practical .NET

Logic Is Your Enemy

Leveraging the right combination of object-oriented tools can keep your code simple, even as the problems you solve get more complicated.

One way to measure the complexity of your code is the amount of If…Then logic it contains. After all, each If…Then statement doubles the number of paths through the application: one path when the test is true, another when it's false. Two If statements, therefore, create four paths (one path when the first test is true and the second is false, a second path when both tests are true, and so on). Programs with more logic are harder to test, harder to understand and harder to extend. The history of programming can be looked at as a continuous attempt to handle increasingly more complicated problems without writing code that no one will be able to fix or enhance -- in other words, to solve complicated problems with non-complicated code.

For example, in a simple world, you wouldn't need If statements at all. In a simple world, you'd have a Customer object with a method called Buy. You'd pass that Buy method a Product and the number of Products being bought; the Buy method would return the price to charge the Customer. In that world, an application where customer A123 buys three units of product RTFM would look like this:

Dim cust As Customer
cust = New Customer("A123")
sDim prod As Product
prod = new Product("RTFM")
Dim Amount As Decimal
Amount = cust.Buy(Product, 3)

Inside the Customer class' Buy method, to find out what to charge the customer, you'd just call the Product class' GetPrice method, passing the quantity to be bought. That simple version of the Buy method would look like this:

Public Class Customer
  Public Function Buy(prod As Product, quantity As Integer) As Decimal
    Return prod.GetPrice(quantity)
  End Function
End Class

And the Product class' GetPrice method is equally simple because all it has to do is multiply the quantity by the Product's base price:

Public Class Product
  Protected basePrice As Integer
  Public Function GetPrice(Quantity As Integer) As Decimal
    Return basePrice * Quantity
  End Function
End Class

Complications: Two Kinds of Products
But you don't live in a simple world: You live in the real world where things are more complicated. For example, your company has two types of Product objects because some of your products are really services. With services, the quantity represents the number of hours spent on the service rather than the quantity purchased. That's good news: With services you can multiple the "quantity of hours" by the hourly rate, just like a product. But, unlike products, you also have to add on a $50 callout charge to the service's price.

Your Buy method is now driven by the type of Product being bought: Is it a product or service? If you're not careful, your Buy method will get more complicated because it will initially include an If statement to see if what's being bought is a product or a service:

Public Function Buy(prod As Product, quantity As Integer) As Decimal
  If prod.Type = 'Product' Then
    Return prod.GetPrice(quantity)
  Else
    Return prod.GetPrice(quantity) + 50
  End If
End Function

But, eventually, because the world always gets more complicated, the number of different products will increase and you'll "enhance" your GetPrice method by replacing that If statement with a Select Case statement.

But it doesn't have to be that way. Inheritance can keep your code simple. First, you create a ProductBase object with a Price field and a definition for a GetPrice method:

Public MustInherit Class ProductBase
  Protected basePrice As Integer
  Public MustOverride Function GetPrice() As Decimal
End Class

Then you create two kinds of Product classes, both of which inherit from ProductBase. Each of these has an actual GetPrice method that does the right thing for its type of Product. The standard Product object has your original code in it:

Public Class Product
    Inherits ProductBase

  Public Overrides Function GetPrice(Quantity As Integer) As Decimal
    Return MyBase.basePrice * Quantity
  End Function
End Class

Your ServiceProduct class gets more complicated, but only by adding enough code required to support the Callout charge and include that charge in the GetPrice calculation (some complexity, it turns out, is unavoidable):

Public Class ServiceProduct
    Inherits ProductBase
  Private calloutCharge As Integer

  Public Overrides Function GetPrice(Quantity As Integer) As Decimal
    Return MyBase.basePrice * Quantity + 50
  End Function
End Class

You're still looking at some very simple code here.

Not much changes in the Customer class' Buy method. You just change the parameter to the method to accept any kind of Product:

Public Function Buy(prod As ProductBase, Quantity As Integer) As Decimal
  Return prod.GetPrice(Quantity)
End Function

If you're passed a Product object in that ProductBase method, when you call the GetPrice method, you'll get the Product's price calculation; if you're passed a ServiceProduct, when you call the GetPrice method, you'll get the ServiceProduct's calculation. From the Buy method's point of view, it appears that ProductBase has a single method that always does the right thing. You don't need an If statement and you certainly don't need a Select…Case structure -- you just need more simple objects that inherit from ProductBase.

The following code doesn't care if the GetProduct method returns a Product or a ServiceProduct and is just as simple as my first version:

Dim cust As Customer
cust = New Customer('A123')
Dim prod As ProductBase
prod = GetProduct('RTFM')
Dim Amount As Decimal
Amount = cust.Buy(prod, 3)

Here's the great thing: If your company adds a new Product to sell, you don't touch any of this code -- you just create a new class that inherits from ProductBase with its own version of the GetPrice method.

But then the business makes things worse: You now also have two kinds of customers. This means that price is driven by two things: The type of the customer and the type of the product. To handle this problem, you could end up writing more logic with code like this:

If Product is Standard and Customer is Premium Then
ElseIf Product Is Service and Customer is Premium Then
ElseIf Product Is Standard and Customer is Typical Then
...and so on

This is already hard to understand. More critically, with this design, if you add a new Product or a new Customer you'll have to go in and modify this code, hoping that you don't introduce a new bug (and how would you test for that?).

It doesn't have to be that way. Method overloading and the Visitor pattern can keep your code simple. I'll show how later this month.

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

Featured

Subscribe on YouTube