2

I am working on a problem with lots of if-then-else calculations. I am trying to compartmentalize the logic to make it more maintainable and less error prone. But, as I try options, I don't see what the best practice should be.

By way of example, suppose I am building an order fulfillment system. One thing that must be done is to calculate and add sales tax (if any), based on some parameters. Let's focus on the question of applying sales tax. Each state has its own rate; some have no tax at all; and some might (for example) charge 50% of the tax to active duty military (which is not true but shows the complexity). For example, here is a stub showing the core problem:

items = {'toaster': {'price': 200, 'weight': 1.5},
         'mixer': {'price': 100, 'weight': 1}}

customers = {'smith': {'military': True, 'state': 'Oregon'},
            'jones': {'military': False, 'state': 'New York'}}

states = {'Oregon': {'taxable_state':False, 'tax_rate': None, 'military_discount': .50},
          'New York': {'taxable_state':True, 'tax_rate': .10, 'military_discount': None}}

customer = 'jones'

taxable_transaction = states[customers[customer]['state']]['taxable_state']

if taxable_transaction:
    # calculate taxes due and add to item price...
    print('transaction is taxable')

What we want to do (where the print statement is) is have a function that takes input values for each order (not shown) and that returns, let's say, the tax due for a given cost of merchandise.

Among people I work with there are three possible ways to skin this cat.

  1. Pass everything to the tax function. In other words, call get_tax_due(items, customers, states, orders).Then the top-level code is simple, but the function needs to know everything about orders, customers, etc.- not just taxes.

  2. Pass as little as possible to the tax function. So, we have a lot of if-then-else in the top-level code, possibly not even needing the modular tax function. (After all, the if-then-else complexity has to go somewhere.)

  3. Somewhere in-between. Have a loop that iterates over orders and items. Make the tax code simpler than in approach #1, and pass it only what it needs to do the processing. So, we might call it like this, conceptually:

    tax_due = get_tax_due(states.Oregon.taxable_state, states.Oregon.tax_rate, states.Oregon.military_discount...)

after looking up the order for the customer. So, for instance, the code to calculate tax due doesn't even get called in a non-tax state. One unfortunate thing about this approach is that you get things like the statement:

taxable_transaction = states[customers[customer]['state']]['taxable_state']

which is near-impossible to think through (and even harder to code and maintain).

Does anyone know of an answer, or even a resource, laying out the best way to approach a problem like this? It seems a repetitive pattern that ought to have some answers as to which way is easiest to write; which way is easiest to maintain; which way is easiest to test; etc.

2
  • Complex business logic like this is often best expressed in a system that is designed specifically for that task rather than directly in your programming language. A rules engine or even a DSL can change a complex, unreadable mess of domain rules into a description that even domain experts can read. Apr 8 at 19:49
  • There are some great examples of using F#, a functional language, to domain-model issues like this. This video youtu.be/NoGyFQ99NgY delves into currency rates, etc in an ordering system.
    – NWoodsman
    Apr 8 at 21:41

1 Answer 1

4

It helps to define the concepts of the problem you are dealing with. My understanding is that you have a tax policy that is specific to each state and depends on attributes of a customer in order to calculate the tax of an item.

The tax policies seem to be quite simple, if we take them in isolation. Let's define a couple implementations of a TaxPolicy interface:

class NoTaxPolicy:
    def calculate(self, price):
        return 0

class StaticTaxPolicy:
    def __init__(self, rate):
        self.rate = rate

    def calculate(self, price):
        return self.rate * price

# That's the Decorator design pattern
class MilitaryDiscountTaxPolicyDecorator:
    def __init__(self, delegate, discount_rate):
        self.delegate = delegate
        self.discount_rate = discount_rate

    def calculate(self, price):
        return self.discount_rate * self.delegate.calculate(price)

We then want to instantiate the correct tax policy depending on the state. Let's define a way for each state to create the tax policy:

# This is the abstract factory design pattern
class OregonTaxPolicyFactory:
    def create(self, customer):
        state_tax_policy = NoTaxPolicy()

        if customer["military"]:
            # Obviously, in practice, we'd want to apply the discount
            # on a different tax policy than NoTaxPolicy, but I left
            # it that way just to give a rough idea of how it could
            # work if there were an actual policy
            return MilitaryDiscountTaxPolicyDecorator(state_tax_policy, 0.50)

        return state_tax_policy

class NewYorkTaxPolicyFactory:
    def create(self, customer):
        # The tax policy doesn't actually depend on the customer,
        # but the outside world doesn't need to know this!
        return StatixTaxPolicy(0.10)

Finally, we can glue the code together to calculate the item price:

tax_policy_factories = {
    "Oregon": OregonTaxPolicyFactory(),
    "New York": NewYorkTaxPolicyFactory(),
}

item = items["toaster"]
customer = customers["jones"]
state = customer["state"]
tax_policy = tax_policy_factories[state].create(customer)

print(tax_policy.calculate(item["price"]))

You gain multiple benefits this way:

  • Each tax policy implementation is, by itself, super simple to understand;
  • You can add new tax policies and compose them however you want;
  • How the tax policy is defined by a given state is encapsulated so that the code that calculates the taxes does not have to know how the tax policy is defined by a certain state;
  • Once you have a tax policy instance, you no longer care about the customer to calculate the taxes, you just apply the policy, whatever it is. You don't even care if the state has a no tax policy, you'll just get a result of $0 taxes to pay;
  • Everything is simple to test in isolation.
5
  • What do you mean by the "decorator" implementation? I have trouble following what decorators do and how they work.
    – eSurfsnake
    Apr 8 at 20:26
  • My understanding of the military discount you described in your original post is that an active member of the military pays only half the amount of the taxes. Therefore, the decorator takes two arguments when it is instantiated: the original tax policy, and the discount rate. The implementation of the calculate method is to use the original tax policy, calculate the amount, then multiply it with the discount rate. In practice, if the taxes are $10, then an active member of the military will pay only $5. Does that make sense to you? Apr 8 at 20:31
  • Do you need to initialize 50 factories?
    – eSurfsnake
    Apr 8 at 20:46
  • Not necessarily. If multiple states share the same tax policies, it could be reasonable to share the same factories as well. At that point, it depends on what your actual problem is. My answer gives a general idea on one way to solve this problem, but of course, the devil is in the details, and it may need to be tweaked a bit for your use case. Apr 8 at 22:53
  • 1
    The decorator design pattern is much like functional composition but at the object level. You wrap one object inside another. The outside object receives all the messages, decides what ones to pass to the inner object, and makes whatever modifications it likes to the results. In this case, a discount. Since both objects have the same interface nothing else needs to know this is even happening. Apr 9 at 10:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.