Foundations of Software Engineering

Kenneth M. Anderson <kena@cs.colorado.edu>

Lecture 26: Refactoring, Part 2

Credit where Credit is Due

Last Lecture

Goals of this Lecture

Tutorial: Background Information

Does this system need refactoring?

Initial Class Diagram

Before

statement() works by looping through all rentals; for each rental, it retrieves its movie and the number of days it was rented; it also retrieves the price code of the movie

It then calculates the price for each movie rental and the number of frequent renter points and returns the generated statement as a string

Initial Code: Movie

public class Movie {

    public static final int CHILDRENS   = 2;
    public static final int REGULAR     = 0;
    public static final int NEW_RELEASE = 1;

    private String _title;
    private int    _priceCode;

    public Movie(String title, int priceCode) {
        _title = title;
        _priceCode = priceCode;
    }

    public int getPriceCode() {
        return _priceCode;
    }

    public void setPriceCode(int arg) {
        _priceCode = arg;
    }

    public String getTitle() {
        return _title;
    }
}

Initial Code: Rental

public class Rental {
    private Movie _movie;
    private int   _daysRented;

    public Rental(Movie movie, int daysRented) {
        _movie      = movie;
        _daysRented = daysRented;
    }

    public int getDaysRented() {
        return _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }
}

Initial Code: Customer

import java.util.Enumeration;
import java.util.Vector;

public class Customer {
    private String _name;
    private Vector _rentals = new Vector();

    public Customer (String name) {
        _name = name;
    }

    public void addRental(Rental arg) {
        _rentals.addElement(arg);
    }

    public String getName() {
        return _name;
    }

    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;
    }
}

Step 1: Refactor statement()

But WAIT!!!

Our Test Case

import java.io.*;

public class useCustomer {

    private static Movie monty = new Movie("Life of Brian", Movie.REGULAR);
    private static Movie terry = new Movie("Brazil", Movie.REGULAR);
    private static Movie disney = new Movie("Snow White", Movie.CHILDRENS);
    private static Movie nicole = new Movie("Moulin Rouge", Movie.NEW_RELEASE);

    private static Rental r1 = new Rental(monty, 7);
    private static Rental r2 = new Rental(terry, 14);
    private static Rental r3 = new Rental(disney, 4);
    private static Rental r4 = new Rental(nicole, 2);

    private static Customer ken = new Customer("Ken Anderson");

    public static void main(String[] args) {
        try {
            ken.addRental(r1);
            ken.addRental(r2);
            ken.addRental(r3);
            ken.addRental(r4);

            File target = new File(args[0]);

            LineNumberReader in = new LineNumberReader(new FileReader(target));

            String targetStatement = null;
            String line            = null;

            while ((line = in.readLine()) != null) {
                if (targetStatement != null) {
                    targetStatement += line;
                } else {
                    targetStatement = line;
                }
                targetStatement += "\n";
            }
            // strip off last \n
            targetStatement =
                targetStatement.substring(0, targetStatement.length()-1);

            System.out.println("Current Statement:\n\n" + ken.statement());
            System.out.println("");
            System.out.println("");
            System.out.println("Target Statement:\n\n" + targetStatement);
            System.out.println("");

            if (targetStatement.equals(ken.statement())) {
                System.out.println("Test Passed.");
            } else {
                System.out.println("Test Failed.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Our Test Statement

Rental Record for Ken Anderson
	Life of Brian	9.5
	Brazil	20.0
	Snow White	3.0
	Moulin Rouge	6.0
Amount owed is 38.5
You earned 5 frequent renter points

Note: Lines 2-5 have a single tab character both before and after the movie name

Test Run on Original Program

$ java -classpath . useCustomer targetStatement.txt 
Current Statement:

Rental Record for Ken Anderson
        Life of Brian   9.5
        Brazil  20.0
        Snow White      3.0
        Moulin Rouge    6.0
Amount owed is 38.5
You earned 5 frequent renter points


Target Statement:

Rental Record for Ken Anderson
        Life of Brian   9.5
        Brazil  20.0
        Snow White      3.0
        Moulin Rouge    6.0
Amount owed is 38.5
You earned 5 frequent renter points

Test Passed.

Back to Step 1

// 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;
}
private double amountFor(Rental each) {
    double thisAmount = 0;

    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;
    }

    return thisAmount;
}
thisAmount = amountFor(each);

Step 1 in Detail: Original Code

public 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;
    }
…
}

Step 1 in Detail: Extract Method

public 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;
    }
    
    private double amountFor(Rental each) {
        double thisAmount = 0;

        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;
        }

        return thisAmount;
    }

Step 1 in Detail: Update statement to call new method

public String statement() {

    double      totalAmount          = 0;
    int         frequentRenterPoints = 0;
    Enumeration rentals              = _rentals.elements();
    String      result               = &quot;Rental Record for &quot; + getName() + &quot;\n&quot;;

    while (rentals.hasMoreElements()) {

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

        // determine amounts for each line
        thisAmount = amountFor(each);

        // add frequent renter points
        frequentRenterPoints++;

        // add bonus for a two day new release rental
        if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &amp;&amp;
            (each.getDaysRented() &gt; 1)) {
                frequentRenterPoints++;
        }

        // show figures for this rental
        result += &quot;\t&quot; + each.getMovie().getTitle() +
                  &quot;\t&quot; + String.valueOf(thisAmount) + &quot;\n&quot;;
        totalAmount += thisAmount;
    }

    // add footer lines
    result += &quot;Amount owed is &quot; + String.valueOf(totalAmount) + &quot;\n&quot;;
    result += &quot;You earned &quot; + String.valueOf(frequentRenterPoints) +
              &quot; frequent renter points&quot;;
    return result;
}

Possible Pitfall

$ java -classpath . useCustomer targetStatement.txt 
Current Statement:

Rental Record for Ken Anderson
        Life of Brian   9.0
        Brazil  20.0
        Snow White      2.0
        Moulin Rouge    6.0
Amount owed is 37.0
You earned 5 frequent renter points


Target Statement:

Rental Record for Ken Anderson
        Life of Brian   9.5
        Brazil  20.0
        Snow White      3.0
        Moulin Rouge    6.0
Amount owed is 38.5
You earned 5 frequent renter points

Test Failed.

Step 2: Rename variables

private double amountFor(Rental aRental) {
    double result = 0;

    switch (aRental.getMovie().getPriceCode()) {
        case Movie.REGULAR:
            result += 2;
            if (aRental.getDaysRented() > 2) {
                result += (aRental.getDaysRented() - 2) * 1.5;
            }
            break;
        case Movie.NEW_RELEASE:
            result += aRental.getDaysRented() * 3;
            break;
        case Movie.CHILDRENS:
            result += 1.5;
            if (aRental.getDaysRented() > 3) {
                result += (aRental.getDaysRented() - 3) * 1.5;
            }
            break;
    }

    return result;
}

Step 3: Move Method

public class Rental {
…
    public double getCharge() {

        double result = 0;

        switch (getMovie().getPriceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (getDaysRented() > 2) {
                    result += (getDaysRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW_RELEASE:
                result += getDaysRented() * 3;
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (getDaysRented() > 3) {
                    result += (getDaysRented() - 3) * 1.5;
                }
                break;
        }

        return result;
    }
…
}

New Class Diagram

Step3 Classdiagram

No major changes; however Customer is now a smaller class and Rental has stopped being “just a data holder” and can now actually do something!

Definitely making progress!

Step 4: Replace Temp with Query

public String statement() {

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

    while (rentals.hasMoreElements()) {

        Rental each       = (Rental) rentals.nextElement();

        // 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(each.getCharge()) + "\n";
        totalAmount += each.getCharge();
    }

    // add footer lines
    result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
    result += "You earned " + String.valueOf(frequentRenterPoints) +
              " frequent renter points";
    return result;
}

Step 5: Frequent Renter Points

// 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++;
}
public int getFrequentRenterPoints(Rental each) {
    if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
        (each.getDaysRented() > 1)) {
        return 2;
    } else {
        return 1;
    }
}
frequentRenterPoints += getFrequentRenterPoints(each);
public int getFrequentRenterPoints() {
    if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
        (getDaysRented() > 1)) {
        return 2;
    } else {
        return 1;
    }
}
public String statement() {

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

    while (rentals.hasMoreElements()) {

        Rental each       = (Rental) rentals.nextElement();

        frequentRenterPoints += each.getFrequentRenterPoints();

        // show figures for this rental
        result += "\t" + each.getMovie().getTitle() +
                  "\t" + String.valueOf(each.getCharge()) + "\n";
        totalAmount += each.getCharge();
    }

    // add footer lines
    result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
    result += "You earned " + String.valueOf(frequentRenterPoints) +
              " frequent renter points";
    return result;
}

New Class Diagram

Step5b Classdiagram

Customer continues to get smaller (but with better structure)

Rental continues to get larger (but more useful)

Step 6: Remove Temp Variables

New Class Diagram

Step6b Classdiagram

Customer's structure continues to evolve; it now has two methods that can be reused across statement() and htmlstatement()

Note: statement()'s algorithm has now evolved. It used to perform the loop only once, now it performs it three times! Is this a performance problem? Maybe, we can't say without checking the program with a profiler

Step 7: add htmlstatement()

Outline of Form Template Method Solution

FormTemplateMethod

Steps of Solution: Create new class hierarchy containing a single version of the duplicated method. Extract the portions that vary into new smaller methods. These methods are declared abstract on the superclass. The original method invokes these abstract methods at the appropriate place. These abstract methods are implemented in the subclasses.

New Requirements

Step 8: Move Methods

New Class Diagram

Step8b Classdiagram

Movie has new methods, finally making the transition from data holder to object; these methods will allow us to handle new types of movie easily

How to handle new Movies?

Movies Take1

But movies can change type! A children’s movie when it is first released is a “new release”; later it becomes a children’s movie. So this approach won’t work!

State pattern to the rescue!

Movies Take2

A movie has a particular state: its charge (and its renter points) depend on that state; so we can use the state pattern to handle new types of movies (for now, at least)

Note: In this formulation, the differences between the State pattern and the Strategy pattern we saw in a previous lecture are negligible

Step 9: Replace Type Code with State/Strategy

Step 10: Move Method

Step 11: Replace Conditional with Polymorphism

Step 12: Handle Renter Points

We’re done!

Final class diagram

After

Refactoring

Coming Up Next