Discount rules, part 3
I originally thought that this example would stretch to two articles. But this is part 3, and I have drafts of two more queued up. Who knew such a simple problem would yield so many insights! Anyway…
In part 1 we discovered some implicit coupling in a Python application I prepared long ago for just such a purpose; and in part 2 we thought about how we might approach choosing an appropriate fix, but only for the simpler case of two copies of the magic numbers.
So now let’s factor in our most recent discovery (also from part 1), that the Catalogue
class also knows these magic numbers.
Before we thought about the coupling with the Catalogue
class we had whittled our options down (in part 2) to these three solution designs:
Have
DisplayBasketCommand
fetch the values explicitly fromBasket
.Have
Basket
fetch the values explicitly fromDisplayBasketCommand
.Keep the values in some third thing — let’s call it
DiscountRules
— and have Basket andDisplayBasketCommand
either fetch them or have them injected.
But now, with Catalogue in the picture, we have to modify some of these a little:
Have
DisplayBasketCommand
fetch the values explicitly fromBasket
, which in turn fetches them fromCatalogue
.<… Five similar options in which the three classes delegate between themselves …>
Keep the values in some third thing — let’s call it
DiscountRules
— and haveBasket
,Catalogue
andDisplayBasketCommand
either fetch them or have them injected.
As we did in part 2, this is where we need to think about intention revealing code.
We really only have two levers to pull when it comes to having the code reveal our intentions: the names we give to things, and the set of things we choose to give names to. Together, these names should create a narrative that reads well to anyone who understands our domain.
So for example any solution in which we have Catalogue
inside Basket
or vice versa doesn’t fit those criteria. Likewise solutions that involve having Catalogue
know about Basket
, or Basket
know about DisplayBasketCommand
, don’t make “domain sense” either.
Which leaves us, realistically, with one option:
And now that we’ve decided what shape we want to see for the coupling, we can make some choices about how to implement this design in the code.
I create a DiscountRules
class and initialise any instance with the threshold (2000) and discount rate (10%):
class DiscountRules:
def __init__(self):
self.threshold = 2000
self.rate = 10
def applyTo(self, total):
if total > self.threshold:
return total - (total/self.rate)
return total
def applyAndDisplay(self, total):
if total > self.threshold:
discount = total/self.rate
print("$%8.02f %d%% discount" % (discount / -100.0, self.rate))
return total - discount
return total
def describe(self):
return ("%d%% discount on orders over $%5.02f!\n" % (self.rate, self.threshold / 100.0))
And now I use it wherever those magic numbers previously appeared. Firstly in Basket
:
class Basket:
def _currentTotal(self):
total = 0
for item in self.items.values():
total += item.count * item.price
total = DiscountRules().applyTo(total)
return total
Now in Catalogue
:
class Catalogue:
#...
def list(self, out):
for sku in self._sortedSkus():
out.write("%s\t%dp\t%s\n" % (sku.code[2], sku.price, sku.title))
out.write("\n%s\n" % DiscountRules().describe())
And finally in DisplayBasketCommand
:
class DisplayBasketCommand:
def run(self, cmd):
items = self.basket.list()
for item in items:
print item.price + " " + item.title
total = self._basketTotal(items)
total = DiscountRules().applyAndDisplay(total)
print "---------------"
print "$%8.02f total\n" % (total)
In each case, by creating and using a new instance of the DiscountRules
class, the calling code doesn’t need to know either the magic numbers or indeed how they are used to calculate the discount.
So by introducing a new group of names I have improved the intentionality of these three classes. This makes it clearer in each case that there is a dependency on something called DiscountRules
which provides methods we can use instead of copying around that discounting algorithm.
As a result there is now much less implicit coupling. But it hasn’t gone away completely, as we shall see next time.