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.
Then in part 3 we created a DiscountRules
class to encapsulate those magic numbers. This had the distinct benefit of giving names to the algorithms that involve those numbers.
But the intention of the business rules is that the same rules — with the same thresholds and the same discount rates — should be used throughout the application. Here’s a sample run:
Welcome to our little corner shop!
For help, type 'h' or 'help' or '?'
shop> p
6000 399p Phillips screwdriver, small, with colour coded handle
1045 120p Finest brie, 100g
2761 50p Raffle ticket
5990 4699p Top hat, black, large
10% discount on orders over $20.00!
shop> a 5990
shop> b
$ 46.99 1x Top hat, black, large
$ -4.70 10% discount
---------------
$ 42.29 total
shop> c
All items checked out. Total price $42.29
shop> q
Goodbye. Thanks for your custom!
The p
command lists the catalogue, the a
command adds an item to the basket, b
displays the basket contents, and c
invokes checkout. Clearly if our Basket
, Catalogue
and DisplayBasketCommand
objects used non-identical discounting rules, the user would be surprised at best.
And yet the implementation I did in part 3 allows exactly that. There is nothing requiring these three classes to use the same DiscountRules
object, so we still have Implicit Coupling between our three application classes. The only thing we achieved last time was to give names to our discount-related actions.
Actually, that last sentence isn’t true. By hiding those numbers inside a class with three methods, we wrapped them in constructs that allow us to share a single instance of them.
In order to explicitly ensure that the Basket
, Catalogue
and DisplayBasketCommand
objects use precisely the same discount rules, we can now arrange for exactly one place in our application to instantiate the rules and then inject them into Basket
, Catalogue
and DisplayBasketCommand
.
Fortunately the application’s main
function is close enough to the action to allow that to happen. We change it from this:
def main(self):
warehouse = Warehouse.fromFile("../warehouse.dat")
catalogue = Catalogue.fromFile("../catalogue.dat")
basket = Basket()
catalogueActions = CatalogueActions(catalogue)
warehouseActions = WarehouseActions(warehouse, catalogue)
basketActions = BasketActions(basket, catalogue, warehouse)
self.displayWelcomeMessage()
UserInterface(
catalogueActions,
warehouseActions,
basketActions
).start()
self.displayGoodbyeMessage()
sys.exit()
To this:
def main(self):
rules = DiscountRules()
warehouse = Warehouse.fromFile("../warehouse.dat")
catalogue = Catalogue.fromFile("../catalogue.dat", rules)
basket = Basket(rules)
catalogueActions = CatalogueActions(catalogue)
warehouseActions = WarehouseActions(warehouse, catalogue)
basketActions = BasketActions(basket, catalogue, warehouse, rules)
self.displayWelcomeMessage()
UserInterface(
catalogueActions,
warehouseActions,
basketActions
).start()
self.displayGoodbyeMessage()
sys.exit()
This single function now guarantees that the same discount rules are used everywhere, and that fact can be read explicitly from this one place in the code.
For those of you who like models and diagrams, I can represent this new solution in (something like) UML:
(The ellipses represent the Python “duck” types implied by the way the DiscountRules
object is used in each of the three classes.)
By arranging the creation of our domain objects like this we have replaced the implicit coupling we found in part 1 with explicit coupling: The code of the main
function now describes, in one place, exactly which rules the other objects apply in each case.
An insightful set of articles Kevin, thank you. I particularly like this from #3:
"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."
I think there is a lot in this paragraph. I've long believed that a 'good' design will look roughly the same whether you derive it from the bottom up (i.e. by reducing implicit coupling, reducing repeated code, etc.) or the top down (finding the names that make sense within the business domain). My experience of such designs is they tend to survive both business and technical change better than ones that only make sense when viewed from one direction or the other (or neither!).
I've worked on a lot of systems of they type you're using as an example and concepts like catalogue and discount rule are always where you end up as they result in the cleanest code *and* have the most direct correspondence with the business domain. This shouldn't be a surprise; we have millennia of experience of pricing and selling things so we've already put a lot of effort into ensuring consistent and correct calculation of prices.
The reason I think this is important in this context is that us techies spend a lot of time thinking about what we refactor from (code smells) and the refactoring we apply (transformations such as extract method, etc.), but less time thinking about where we're refactoring to.
Again, in my experience, identifying code smells and applying common transformations will lead you to a better place, but aren't *necessarily* going to help you find that 'good' design. However, looking for transformations that fix code smells whilst taking steps towards a domain concept will lead to more maintainable code; code that is more accommodating of change whether than change is prompted by business or technical requirements
Looking ahead slightly to a more sophisticated iteration of your example, you can imagine a general set of pricing rules to deal with sales taxes, shipping costs, credit notes, perhaps surge pricing for digital assets. We shouldn't try to preempt these in the design if we don't need it today but as the need for each of these becomes apparent the domain gives us clues as to where our refactoring should go. And when you have multiple pricing rules, something is required to know what order to apply them in and which rules can't be combined. In the domain of over-the-counter sales this used to be the cashier, or a the sales person for more bespoke selling, until they were gradually superseded by software that acts as the 'pricing calculator' which is dominant domain concept today.