This refactoring example is from Chapter 1 of Refactoring: Improving the Design of Existing Code by Martin Fowler.
There are two branches in this repository:
java
contains Java source code, updated to use current Java featurespython
contains Python source, translated from the Java code
The Java code has been updated to use features such as a List with type
parameter instead of Vector and type casts, and the for-each loop.
I also removed the leading underscore on attribute names (name
instead of _name
),
which is more typical of current coding conventions.
The runnable Main class (or Python main.py
) creates a customer and prints
a statement.
The PDF from Chapter 1 explains the motivation for each refactoring and how to do it.
- Check out either the
java
orpython
branch (your choice). - Perform the 5 refactorings listed below, and optionally the 6th "missing" refactoring.
- Before starting to refactor, run the units tests. They should all pass.
- After each refactoring run the unit tests again. They should still pass.
The refactorings are (filenames refer to Java version):
-
Extract Method. In Customer.statement() extract the code that calculates the price of each rental.
- Make it a separate method.
-
Move Method. After extracting a method to calculate the price of a rental, Fowler observes that the method uses information about the rental but not about the customer. Hence, the method should be in the
Rental
class instead ofCustomer
class.- Move the method to the
Rental
class. A good IDE has a refactoring tool to do this for you. - After the change, verify that the method is referenced correctly in code. It changes from:
// Customer class: public double amountFor(Rental rental) { ... } charge = amountFor(rental);
to:
// Rental class: public double getCharge() { ... } // Customer class: charge = rental.getCharge();
- write a unit test for this method.
- Move the method to the
-
Replace Temp Variable with a Query. Instead of using
charge = rental.getCharge()
(assign to a temp variable) and usingcharge
in the code, directly invokerental.getCharge()
wherever the value is needed.- This removes the local variable but results to multiple method calls for the same thing.
- Personally, I prefer using a temporary variable instead of duplicate method calls.
-
Extract Method. Refactor summation of frequent renter points to a separate method.
- write a unit test for this new method
-
Replace Conditional Logic with Polymorphism. Replaces the "switch" statement for movie price codes with polymorphism, in two steps.
- The first step is to make the Movie class compute its own frequent renter points.
- The second step is have it delegate that task to a Strategy object.
- You define an interface (e.g. PriceStrategy) and concrete implementations for RegularPrice, ChildrensPrice, NewReleasePrice. The strategy interface also computes frequent renter points.
- Replace the constant for price code with objects from the strategy classes.
- In Fowler's article, this is a long refactoring because he first uses inheritance and then explains why that's a poor solution.
- This refactoring uses the design principle "Prefer composition over inheritance".
-
The Missing Refactoring. In the final code the
Customer
class still needs a Move Method refactoring to remove some unrelated behavior, in my opinion.- What do you think?
In Python, the refactoring are the same, but some details are different.
- method names should use Python naming convention
- Python does not require creating an interface for strategy. If you want to write code like Java, you can create an abstract superclass (
PriceStrategy
) for the interface with methods that return 0.RegularPrice
, etc., are concrete subclasses ofPriceStrategy
. - Another way to implement Strategy in Python is to use an Enum.
- Each member of the enum is one pricing strategy (normal, childrens, new_release).
- Each enum member is a dict, and the values in the dict are lambdas to compute the price and frequent renter points. In this way, each number member can define it's own function for pricing and frequent renter points.
from enum import Enum class PriceCode(Enum): """An enumeration for different kinds of movies and their behavior""" new_release = { "price": lambda days: 3.0*days, "frp": lambda days: days } normal = { "price": lambda days: ..., "frp": lambda days: ... } childrens = { ... } def price(self, days: int) -> float: "Return the rental price for a given number of days""" pricing = self.value["price"] # the enum member's price formula return pricing(days)
- The Enum provides methods for
price
and renter points, and delegates those methods to the enum member (which is referenced byself
). Theprice
metho (shown above) uses the enum member's dict (values
) to get a lambda expression it should use to compute the rental price, then uses that lambda to compute the actual price. - To reference a member of the PriceCode enum, you write:
movie_type = PriceCode.new_release # invoke a method of PriceCode print("Rental price for 3 days:", movie_type.price(3))
- Refactoring, First Example extract from Martin Fowler's Refactoring book.
- Refactoring slides from U. Colorado step-by-step instructions for Java version of this example, including UML class diagram of progress.