I’ve written about Smalltalk before, after my adventures with GNU Smalltalk, I looked at Pharo, which is definitely a much more traditional Smalltalk environment.

Pharo

I had a go at the VideoStore refactoring exercise from the original Refactoring book.

(It was revisited in 2018 in JavaScript for the 2nd Edition)

It involves refactoring a method for generating a statement for a customers video rentals (yeah, I guess they were still a thing in 1999).

I’ll be honest, it’s hard to replicate the code from the book, not because Smalltalk isn’t as expressive as the Java from the book, but because it’s hard to write the code that is a switch statement in the book in Smalltalk without the urge to replace it with something better…

The original Java…

class Customer {
    public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";

        while (rentals.hasMoreElements()) {
            double thisAmount = 0;
            Rental each = (Rental) rentals.nextElement();

            //determine amounts for each line
            switch (each.getMovie().getPriceCode()) {
                case Movie.REGULAR: thisAmount += 2;
                    if (each.getDaysRented() > 2)
                        thisAmount += (each.getDaysRented() - 2) * 1.5;
                    break;
                case Movie.NEW_RELEASE:
                    thisAmount += each.getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:
                    thisAmount += 1.5;
                    if (each.getDaysRented() > 3)
                        thisAmount += (each.getDaysRented() - 3) * 1.5;
                    break;
            }
            // add frequent renter points
            frequentRenterPoints ++;
            // add bonus for a two day new release rental
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
                frequentRenterPoints ++;
            //show figures for this rental
            result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;
        }
        //add footer lines
        result += "Amount owed is " + String.valueOf(totalAmount) + \n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }
}

Which is fairly simply translated into Smalltalk (I used symbols instead of Enums for the rental types).

Customer >> statement [
	"I calculate and return a statement showing the costs of renting films."
	| totalAmount points thisAmount |
	totalAmount := 0.
	points := 0.
	^ String streamContents: [ :aStream | 
		  aStream << ('Rental record for {1}' format: { name }); cr.

		  rentals do: [ :rental | 
			  thisAmount := 0.		  
			  rental movie priceCode = #regular ifTrue: [ 
				  thisAmount := thisAmount + 2 ].
			  (rental movie priceCode = #regular and: [ rental daysRented > 2 ]) 
				  ifTrue: [ 
				  thisAmount := thisAmount + (rental daysRented - 2 * 1.5) ].

			  rental movie priceCode = #new ifTrue: [ 
				  thisAmount := thisAmount + rental daysRented * 3 ].

			  rental movie priceCode = #childrens ifTrue: [ 
				  thisAmount := thisAmount + 1.5 ].
			  (rental movie priceCode = #childrens and: [ 
				   rental daysRented > 3 ]) ifTrue: [ 
				  thisAmount := thisAmount + (rental daysRented - 3) * 1.5 ].

			  aStream
				  tab;
				  nextPutAll: rental movie title;
				  tab;
				  tab;
				  nextPutAll: thisAmount printString;
				  cr.
			  totalAmount := totalAmount + thisAmount.

			  points := points + 1.

			  (rental movie priceCode = #new and: [ rental daysRented > 1 ]) 
				  ifTrue: [ points := points + 1 ]
			].

		  aStream << ('Amount owed is ${1}' format: { totalAmount }); cr.
		  aStream << ('You earned {1} frequent renter points' format: { points }); cr
	]
]

Of course, it starts with tests…

CustomerTest >> testWithoutAnyRentals [
	self
		assert: customer statement
		equals:
			'Rental record for Martin' , String cr , 'Amount owed is $0'
			, String cr , 'You earned 0 frequent renter points' , String cr
]

CustomerTest >> testWithOneRegularRentalForThreeDays [
	| statement |
	customer addRental: (Rental withMovie: regularMovie daysRented: 3).

	statement := customer statement.

	self assert: (statement includesSubstring: 'Amount owed is $3.5').
	self assert:
		(statement includesSubstring: 'You earned 1 frequent renter points')
]

CustomerTest >> testWithOneRegularRentalForTwoDays [
	| statement |
	customer addRental: (Rental withMovie: regularMovie daysRented: 2).

	statement := customer statement.

	self assert: (statement includesSubstring: 'Amount owed is $2').
	self assert:
		(statement includesSubstring: 'You earned 1 frequent renter points')
]

I’ll look at how to step through the same refactoring process as Martin Fowler covers in the book, using Pharo in a series of steps.