Modeling discounted prices
TL;DR
In this exercise, we work with a more complicated scenario involving discounted prices.
We'll introduce additional classes to represent discounted prices, and also a common, abstract Price
class at the root of the Price class hierarchy.
Requirements
We continue with the requirement:
A price can also be discounted either by a fixed amount of money, or by a percentage.
Sometimes we are confronted with a complex scenario, which may be impossible to solve in a few small steps. Suppose we are asked to handle both fixed and percentage discounts.
price := (100 euros asPrice discountedBy: 10 euros) discountedBy: 10 percent. self assert: price = 81 euros asPrice. price.
In this (unimplemented) snippet, we create a concrete price of a 100 euros, and then discount it twice , first by a fixed amount of 10 euros, and then by 10%.
If we were to try to directly specify this as an example, we would not know where to start coding. Instead let's break this down.
Discount by amount
We can start with the initial price, which is already and example, and discount it.
We'd like to write the following, but we'll need a new class first.
price := PriceExamples new hundredEuros. price discountedBy: 10 euros.
We need something like this:
PriceDiscountedByMoney new price: PriceExamples new hundredEuros; discountMoney: 10 euros.
This means we need a
new
kind of Price class. It's not yet clear what our price hierarchy should look like, so let's start by making this new class a subclass of our existing ConcretePrice
class.
Task: Fix this, creating the new class as a subclass of ConcretePrice
, with slots and accessors for price
and discountMoney
.
Hint: As before, create the slots first, and then use the Create accessors refactoring to generate the accessors. You'll have to apply two refactorings, one for each slot.
If we inspect the result, we see that the Money
view is broken, and shows us nil. The problem is that the money
slot is not initialized because we don't need it. Instead of returning the slot value, we should discount it from the price
slot.
Task: Prototype the expression to compute the discounted money
value in the playground, and then extract it as a new money
method.
Hint: You might have to refresh the view explicitly by clicking on the grey triangle between this pane and the Inspector pane at the right.
Now we should see something like this:
Finally we can implement discountedBy:
.
Task: Implement discountedBy:
in the
Meta
view of an instance of 100 euros asPrice
.
Hint:
All you have to do is return an instance of PriceDiscountedByMoney
(as we did above) but initialized with the correct price
and discountMoney
. You might want to prototype this in the playground first.
price := 100 euros asPrice. price discountedBy: 10 euros.
We'd like to have this as a new example in our PriceExamples
class.
Task: Extract the following as a new example called discountedPriceByAmount
.
Hint: Select all the code and right-click to apply the Extract example refactoring.
price := PriceExamples new hundredEuros. price discountedBy: 10 euros.
You should end up with something that looks like this:
PriceExamples new discountedPriceByAmount.
We aren't testing anything yet.
Task: Rewrite the example to assert that the discounted price is indeed 90 Euros, before returning it.
Refactoring the Price classes
Since the money
slot is not used in the discounted price, we realize we need a new abstract Price
class as a common parent, with money
being abstract.
Task: Perform a
Convert to sibling
refactoring on ConcretePrice
.
Hint:
This refactoring is only available in the Coder. You can get there quickly by clicking on the
Browse class
button of the discounted price example, and then
maximizing
the Coder view (click the +
). Then select ConcretePrice
and right-click to select the
Convert to sibling
refactoring.
After refactoring, ConcretePrice
and PriceDiscountedByMoney
should have a common superclass Price
.
Our examples should still work.
PriceExamples new discountedPriceByAmount.
There are still some things to clean up.
Task: Push the money:
setter and then the money
slot down from Price
to ConcretePrice
, but not to PriceDiscountedByMoney
. Leave the money
method as abstract (self subclassResponsibility
) in Price
.
Hint:
To Push down
a method, right-click on the method name in the method definition. You can then find
Push down
refactoring for a slot in the class definition by right-clicking on the slot. The refactoring transformation is displayed as a tree. Open it and
deselect
PriceDiscountedByMoney
so the method or slot will only be pushed down to ConcretePrice
.
Task: Go to the PriceExamples
class and check that all the examples are still green.
Hint:
Use the Run Examples
action next to the spotter.
Discount by percentage
Now we would like to apply the 10% discount. We want something like this:
priceDiscountedByMoney := PriceExamples new discountedPriceByAmount. price := priceDiscountedByMoney discountedBy: 10 percent.
This generates an answer, but of course it's wrong.
We see now that discountedBy:
needs to double dispatch on the argument, which may be either a money or a Fraction
Task: Reimplement Price>>#discountedBy:
to double dispatch on the argument.
Hint:
The argument should be aMoneyOrPercent
. Then return ^ aMoneyOrPercent discountFrom: self
. You will then have to implement discountFrom:
in both GtTMoney
and Fraction
as extension methods. First just do it in GtTMoney
, and then check that the examples are still green.
NB:
Don't forget to change the method category of discountFrom:
to *EDDPrices
to make it a class extension!
We need a new class PriceDiscountedByPercent
. It would be similar to PriceDiscountedByMoney
so let's first create it as a subclass PriceDiscountedByMoney
.
We would like to create instances of the new percent discount like this.
PriceDiscountedByPercent new price: 100 euros asPrice; discountPercent: 10 percent.
Task: Apply the fixits, making PriceDiscountedByPercent
a subclass of PriceDiscountedByMoney
. Fix the implementation of money
in the new class, prototyping it first in the Playground.
Task: Now implement discountFrom:
in Fraction
as an extension method.
Now the snippet should work:
priceDiscountedByMoney := PriceExamples new discountedPriceByAmount. price := priceDiscountedByMoney discountedBy: 10 percent.
Task: Perform an
Extract example
refactoring, adding it to PriceExamples
.
The result should look like this:
PriceExamples new priceDiscountedByAmountAndPercent.
Task: Adapt the example to add the obvious assertion.
Followup
We still have these requirements:
All operations can be combined arbitrarily. And for audit purposes, we want to track all operations that lead to a concrete amount of money.
Extra Task: Create examples to verify that operations can be combined.
Extra Task: Provide a view that shows how discounts are composed.