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 Number subclass: #Fraction instanceVariableNames: 'numerator denominator' classVariableNames: '' package: 'Kernel-Numbers'

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 Object subclass: #GtTMoney instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Tutorial-Prices-Model' and Fraction Number subclass: #Fraction instanceVariableNames: 'numerator denominator' classVariableNames: '' package: 'Kernel-Numbers' as extension methods. First just do it in GtTMoney Object subclass: #GtTMoney instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Tutorial-Prices-Model' , 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 Number subclass: #Fraction instanceVariableNames: 'numerator denominator' classVariableNames: '' package: 'Kernel-Numbers' 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.