Some OO Design

"The critical design tool for software development is a mind well educated in design principles. It is not the UML or any other technology." - Craig Larman

I've recently found the following interview question on stackoverflow:

Basic sales tax is applicable at a rate of 10% on all goods, except books, food, and medical products that are exempt. Import duty is an additional sales tax applicable on all imported goods at a rate of 5%, with no exemptions. When I purchase items I receive a receipt which lists the name of all the items and their price (including tax), finishing with the total cost of the items, and the total amounts of sales taxes paid. The rounding rules for sales tax are that for a tax rate of n%, a shelf price of p contains (np/100 rounded up to the nearest 0.05) amount of sales tax.

This is a very familiar example that can be used as a really good gauge of various software development skills. There are many different ways to solve this problem. I'll use object-oriented techniques and the Java language to show one such possibility. I'll not show a step-by-step process, just some ideas on how I got to the end result.

Introduction

Before writing any line of code (even tests), I like to think about the main entities involved and broadly how they're connected. This can be seen as a very high level Domain Model that will support the described scenario and serve as the foundation for any subsequent requirement.

At a high level, there are Products, Taxes and Orders. I can put Products into Orders, where Taxes will be applied; and I can get a detailed Receipt for all Entries in the Order.

Then, I'll create the first automated acceptance test, what will drive my design choices. For that, I'll create a couple of Taxes, Products, and an Order.

Products

I'll start with the Products. From the requirements, Products have a Name, a Type (or Category), an indication of whether they're Imported, and a Price. This is how I'd like to create one:

Product ddd = Product.create("Domain Driven Design", ProductType.BOOK, ProductOrigin.LOCAL, dollars("69.99"));

There are many assumptions and simplifications here. First, I'm representing ProductType as a simple enumeration:

public enum ProductType {
  BOOK,
  FOOD,
  MEDICAL,
  MOVIE
}

This fixes the types of product in the code. It's more sensible to think that new types can be created at runtime, and that ProductType deserves a class of its own. But, since I want to focus on the sample scenario and finish my acceptance test, I'll just live with fixed types for now. Also, different types of products could have different properties; Movies can have Length and Cast, Books can have Authors and NumberOfPages. For fixed types this could be modeled with Product subclasses; for dynamic types more complicated DynamicProperties (pdf) could be used, with an effort to find the least amount of property "bags" that would describe most product types. These property bags could also be created as subclasses of some ProductProperties class.

The ProductOrigin enum indicates if a product is imported or not:

public enum ProductOrigin {
  LOCAL,
  IMPORTED
}

I could have used a simple boolean, but the enum makes the code readily understandable.

For the price, I'll use a simple Money class, which is a ValueObject that stores an amount as a BigDecimal. Money is a valuable abstraction; it's more work than using raw BigDecimals, but it improves the intention of the code. It's the simpler thing, not the faster thing. Since all my values will be in dollars, I don't even include a Currency, and leave the constructor private to control instantiation through the dollars factory method, which I statically imported to write the Product creation statement. Also, I set the number of decimal places to 2 (the usual for dollars) rounding up.

public class Money {

  public static Money dollars(String amount) {
    return new Money(new BigDecimal(amount));
  }

  private final BigDecimal amount;

  private Money(BigDecimal amount) { 
    this.amount = amount.setScale(2, RoundingMode.UP);
  }

  ...

}

Prices are one of the most volatile Product properties. They can change with time, with store or market; they can have discounts, different values in different currencies and many other variations. The price associated with a product can be seen as its BasePrice, which at a minimum should be a TemporalProperty. But, for the time being, I don't need to address these concerns, but it's still good to keep them in mind.

Taxes

Now I can tackle taxes:

Basic sales tax is applicable at a rate of 10% on all goods, except books, food, and medical products that are exempt.

The first part is easy. Taxes have a Name and a Rate:

Tax basicSalesTax = Tax.create("Basic sales tax", "0.10");

The Rate is stored as a BigDecimal. Doubles and Floats are never suitable for financial calculations, even when used as rates.

But Taxes are not universally applicable, and there needs to be some kind of TaxEligibilityCheck to find out if a Tax should be applied to a Product (actually, it's better to check if it should be applied to an Entry, as eligibility could be based on factors other than Product properties).

One way to do this is to have an abstract method boolean isEligible(OrderEntry entry) (I'm calling Entry an OrderEntry in the code) in Tax, and have a subclass for each different eligibility criteria. The basic sales tax could then be created like this:

Tax basicSalesTax = TaxWithExemptionsOnProductType.
  create(
    "Basic sales tax", "0.10", 
    ProductType.BOOK, ProductType.FOOD, ProductType.MEDICAL);

Here, I'm assuming there's a Tax subclass named TaxWithExemptionsOnProductType that overrides isEligible to check the product type against the given exempt types. I'd also have a subclass named TaxForImportedProducts that's only applicable to imported products. But, if I wanted a tax that's applicable to imported products with certain product type exemptions, it would be awkward. I'd have to start mixing the logic of both classes and possibly create a third one that combines them. No, I'd like to keep each eligibility check Strategy separate, with the possibility of combining them for Composite criteria. Maybe I'll never need criteria different than the ones in the example, and maybe I'll never need to combine them. But, clearly delineating the responsibilities in the design makes it simpler, easier to understand and easier to evolve. Again, the simpler thing is not the faster thing. And so, I need a new abstraction:

public interface TaxEligibilityCheck {
  boolean isEligible(OrderEntry entry);
}

And Tax will now have a TaxEligibilityCheck instance.

I can already imagine using the Specification pattern to create Composite criteria based on logical operators:

Tax basicSalesTax = Tax.
  create(
    "Basic sales tax", "0.10", 
    and(exempt(ProductType.BOOK), exempt(ProductType.FOOD), not(imported())));

This very flexible approach is a possibility with the TaxEligibilityCheck interface. I couldn't get there easily with subclasses. Finding the right balance between current needs and possible future needs is always difficult, and you should never create superfluous or speculative elements. But it's a win-win situation when a more flexible design is even simpler than the obvious, direct design. Following that advice again, I don't need the full Composite Specification pattern right now, so simple one-off implementations will do.

Besides checking for eligibility, the Tax has a Money calculate(Money amount) method that applies its rate to the given amount (I'm also renaming the class to TaxMethod, not to be confused with a calculated tax amount):

public class TaxMethod {

  public static TaxMethod create(String name, String rate, TaxEligibilityCheck eligibilityCheck) {
    return new TaxMethod(name, new BigDecimal(rate), eligibilityCheck);
  }

  private final String name;
  private final BigDecimal rate;
  private final TaxEligibilityCheck eligibilityCheck;

  private TaxMethod(String name, BigDecimal rate, TaxEligibilityCheck eligibilityCheck) {
    this.name = name;
    this.rate = rate;
    this.eligibilityCheck = eligibilityCheck;
  }

  public boolean isEligible(OrderEntry entry) {
    return eligibilityCheck.isEligible(entry);
  }

  public Money calculate(Money amount) {
    return amount.multiply(rate);
  }

  ...

}

Order

With Products and TaxMethods, it's time to create an Order:

Order order = Order.create();
order.addTaxMethod(baseSalesTax);
order.addTaxMethod(importDuty);
order.addProduct(ddd, 1);

I'm adding the tax methods that the Order should use when calculating the tax for products. This could create a kind of temporal coupling, if taxes are calculated at the time products are added, I should first add all tax methods, and then start adding products. Let's eliminate that coupling:

Order order = Order.createWithTaxMethods(baseSalesTax, importDuty);
order.addProduct(ddd, 1);

That's better. Now I'm passing the tax methods to the constructor (ahem, factory method); which must be done before adding any products. Still, it's a little awkward that Orders have to keep a list of TaxMethods. I need some kind of TaxMethod container, something that will let me reuse the same taxes in different orders; a TaxingPractice that will encapsulate the tax methods in use:

TaxingPractice taxingPractice = TaxingPractice.create();
taxingPractice.add(basicSalesTax);
taxingPractice.add(importDuty);

Order order = Order.create(taxingPractice);
order.addProduct(ddd, 1);

Order Entries

The Order needs to store two types of entries: ProductEntries and the derived TaxEntries. Again, there are many ways to do it, and I've chosen to have a common interface for both entries:

public interface OrderEntry {
  Product getProduct();
  Money getAmount();
}

But still I've chosen to keep them in separate collections inside the Order:

private final List<ProductEntry> entries = new ArrayList<ProductEntry>();
private final List<TaxEntry> taxEntries = new ArrayList<TaxEntry>();

This implementation will simplify the calculation of the various totals that I need (SubTotal, TaxTotal and Total); but it's completely encapsulated in Order, and I'd like to be able to change it in the future without impacting any other class. The other possibilities that I see are having a single List<OrderEntry> for both kinds of entries or having a composite Order encapsulating the internal leaf orders ProductOrder and TaxOrder.

When adding products to an order, I calculate taxes right away:

public void add(Product product, int quantity) {
  ProductEntry entry = ProductEntry.create(product, product.getPrice(), quantity);
  entries.add(entry);
  taxEntries.addAll(taxingPractice.apply(entry));
}

The TaxEntries are derived from the created ProductEntry using the TaxingPractice tax methods and put into the taxEntries collection. It's good to keep a link between the product entry and its derived tax entries, so we better know the origin of the calculations and can more easily manipulate the objects, for example: removing a product from an order should also remove its derived tax entries (a "feature" I have not yet implemented).

This is what the TaxingPractice apply method looks like:

public Collection<TaxEntry> apply(ProductEntry entry) {
  List<TaxEntry> entries = new ArrayList<TaxEntry>();
  for (TaxMethod tax : taxes) {
    if (tax.isEligible(entry)) {
      TaxEntry taxEntry = apply(tax, entry);
      entries.add(taxEntry);
    }
  }
  return entries;
}

private TaxEntry apply(TaxMethod tax, ProductEntry entry) {
  Money taxAmount = tax.calculate(entry.getAmount());
  return TaxEntry.create(entry, tax, taxAmount);
}

To calculate totals, then, the Order just needs to sum its entries' amounts:

public Money getSubTotal() {
  return getTotal(entries);
}

public Money getTaxTotal() {
  return getTotal(taxEntries);
}

public Money getTotal() {
  return getSubTotal().add(getTaxTotal());
}

private Money getTotal(Iterable<? extends OrderEntry> entries) {
  Money total = dollars("0.00");
  for (OrderEntry entry : entries) {
    total = total.add(entry.getAmount());
  }
  return total;    
}

Receipt

The information that's in the Order will be used to create a Receipt. But how should that happen? A Receipt features a visual representation of the information contained in the Order, what conceptually could have many different formats. I can think of two ways to do it: let the Receipt derive its information from the Order or let the Order provide its information to the Receipt. I chose to let Order present its information to the Receipt, which will work like a Builder object. To allow for different receipt implementations, I created a Receipt interface:

public interface Receipt {
  void start();
  void printProduct(ProductEntry entry);
  void printTax(TaxEntry entry);
  void printSubTotal(Money subTotal);
  void printTaxTotal(Money taxTotal);
  void printTotal(Money total);
  void end();
}

The Order just provides all the necessary information to the receipt:

public void print(Receipt receipt) {
  receipt.start();
  for (ProductEntry entry : entries) receipt.printProduct(entry);
  for (TaxEntry entry : taxEntries) receipt.printTax(entry);
  receipt.printSubTotal(getSubTotal());
  receipt.printTaxTotal(getTaxTotal());
  receipt.printTotal(getTotal());
  receipt.end();
}

This way, receipts are really independent from orders. I created a very simple receipt that I can use like this to print to the console:

SimpleReceipt receipt = SimpleReceipt.create(System.out);
order.print(receipt);

Putting it all together

An example usage is this:

// configure the system
Product ddd = Product.create("Domain Driven Design", BOOK, LOCAL, dollars("69.99"));
Product goos = Product.create("Growing Object Oriented Software", BOOK, IMPORTED, dollars("49.99"));
Product house1 = Product.create("House M.D. Season 1", MOVIE, LOCAL, dollars("29.99"));
Product house7 = Product.create("House M.D. Season 7", MOVIE, IMPORTED, dollars("34.50"));

TaxMethod basicSalesTax = TaxMethod.create("BST", "0.10", exempt(BOOK, FOOD, MEDICAL));
TaxMethod importDuty = TaxMethod.create("IMD", "0.05", imported());

TaxingPractice taxes = TaxingPractice.create();
taxes.add(basicSalesTax);
taxes.add(importDuty);

// purchase some items
Order order = Order.create(taxes);
order.add(ddd, 1);
order.add(goos, 1);
order.add(house1, 1);
order.add(house7, 1);

// print the receipt
SimpleReceipt receipt = SimpleReceipt.create(System.out);
order.print(receipt);

Which will print this to the console:

------------------ THIS IS YOUR ORDER ------------------
(001)                Domain Driven Design -----   $69.99
(001)    Growing Object Oriented Software -----   $49.99
(001)                 House M.D. Season 1 -----   $29.99
(001)                 House M.D. Season 7 -----   $34.50
(IMD)    Growing Object Oriented Software -----    $2.50
(BST)                 House M.D. Season 1 -----    $3.00
(BST)                 House M.D. Season 7 -----    $3.45
(IMD)                 House M.D. Season 7 -----    $1.73
                                SUB-TOTAL -----  $184.47
                                TAX TOTAL -----   $10.68
                                    TOTAL -----  $195.15
---------------- THANKS FOR CHOOSING US ----------------

Tests

I've also created a couple of acceptance tests while implementing the solution. The tests helped me iterate and refine the design. I didn't create granular unit tests for the classes because the acceptance tests were sufficient to drive the design to this point. It's still a very incomplete solution, and going forward I'd definitely create separate unit tests to evolve some classes independently.

What's left

As I've already mentioned, the solution is still very incomplete. It exercises a happy path scenario; but it already lays a good foundation to build on. A couple things to tackle next would be:

  • Rounding - Event though I'm rounding up all monetary amounts with 2 decimal places, I haven't verified if this suffices for the rounding specified in the requirements.
  • Remove products from an order - the derived taxes should also be removed.
  • Add the same product twice to an order - the existing product entry should be updated to reflect the new quantity for the product; no two product entries should point to the same product.
  • Validation, boundary conditions, design by contract - many values should be checked for null or whether they fall in a predetermined range. For example, quantities should be greater than zero and probably have an upper limit.

Also, further ideas worth exploring might be:

  • Persistence - how would a database to persist all entities change the design?
  • UI - an interactive UI to create orders would be interesting for Customers; same for an administrative UI for the Administrators (to maintain products, taxes, and review orders)

I'm sure you can think of many other possibilities.

Da Code

My solution can be found on github. Take a look and send me your comments and suggestions!

Comments

  1. Hi Jordao,

    First of all let me congratulate you for writing such a wonderful post. I must say that my OO approach has changed dramatically (in a positive way) after going through your post:) I just have one doubt that what could be the reason behind creating Products statically through create method. (pardon me if this seems silly to you, as I am a newbie in java and OOD)...

    ReplyDelete
  2. Hi codemonk,

    Thanks for the compliments. For this example, there's no particular reason to using a factory method to create objects, a constructor would work just fine as well. Be sure to check Effective Java on the subject though; you'll find many reasons to use it and a couple of reasons not to.

    ReplyDelete
  3. Thanks Jordao, for the prompt response :) and continue the good work :D

    ReplyDelete
  4. Hi Jordao,

    Can you share complete application, i need Sales Tax rounded up to the nearest 0.05 and few other stuffs. It would be of great help if you can share full code with me. My id is shan.saxenas@gmail.com

    ReplyDelete
  5. @Shanu: I created this post as an example of OO design, and I didn't carry the design further. But feel free to do it on your own by forking my code on github.

    ReplyDelete
  6. Hey Jordao,

    This is bot here from stackoverflow. We exchanged a few comments in the post where you presented this solution. I managed to use your idea and clubbed it with some of my own. It would be great if we can further discuss this problem over chat as I can really use your expertise for making the design even more extensible . Can you share your id for your proffered IM?

    ReplyDelete
    Replies
    1. I'm sorry bot, I've been very busy lately. Perhaps you can share what you've done in a comment or a blog post and then we can have a (turn-based) discussion (as opposed to real-time).

      Delete
  7. Hi

    Please provide the name of desing pattern that you used.

    ReplyDelete
    Replies
    1. One of the design patterns that I used is the Specification Pattern. Look it up!

      Delete
    2. You can also say that the receipt is a kind of a builder. Look up the Builder Pattern.

      Delete
    3. .... but in the end, it's more about Principles than Patterns. Take a look at the SOLID principles...

      Delete

Post a Comment

Popular posts from this blog

The Acyclic Visitor Pattern

NRoles: An experiment with roles in C#