How we used a state pattern to implement discount rules
The other day, one of our client asked us to add a discount feature to his product scanning application.
The problematic was the fact that each discount can have a different type of mechanic. For example, these discounts can either be a fixed discount for a single product, or a discount for a combination of 2 specific products, or a fixed discount for 2 different products…
In the first place, we thought about implementing it with rails polymorphic classes, meaning one model for each mechanic.
But with this solution each time we would need to add a new mechanic we would have had to add it to a list, and we did not want to add more and more models to our application.
In an attempt to find a correct solution, one of my co-worker suggested that : “You can do it with a state pattern!”
A state pattern
In a word, Wikipedia says:
With the state pattern, a state machine is implemented by implementing each individual state as a derived class of the state pattern interface, and implementing state transitions by invoking methods defined by the pattern’s superclass.
In the following example, we have a Person class, and its different states are the different type of humor that this person can have : happy or sad.
For each state we implement a method named listen_to_music.
In the happy state, the method will return “Happy by Pharell Williams” and in sad state the method will return “Sad song by We The Kings”.
What is great with this pattern, I think, is the way you can easily implement a new state.
On our project, a Person is a Promotion and a state, a discount mechanic.
If you want more information about state pattern here is a great article.
I will not explain all the database schema but to be quick, we have a Basket class which has some products, and we have our promotions (discounts). A Product has a price, a code, a quantity and a Basket. The Basket only has a status.
Finally, promotions have a mechanic and some rules which are contained in a json of parameters (because each mechanic needs its own settings to be correctly applied).
In this state pattern we have two methods for each mechanic: check and apply. So when we add a product to a basket we will check all discounts and if the discount’s check method returns true, then we apply it.
That way, the mechanic’s apply method only changes the price of the product. After checking all promotions we only need to sum each product’s price to get the new basket total.
Adding a new mechanic is now very easy. Furthemore, we can easily implement it with TDD. Indeed, having separated classes and simple methods allows for much simpler testing, resulting in a general ease to implement it with TDD.
Did we solve our problem?
In general, this implementation does a great job and we haven’t had any issues with this pattern. For the first time I implement a state pattern to resolve a feature with in a simple way. I think my implementation is good for the moment but I want to improve it again and again with some refactoring and code review.
So now, I think we will find more contexts in which a state pattern can be used thanks to its testability and easy way to resolve complex problems.