Tuesday, September 19, 2006

Ruby Form Template Method Using Extend

In Martin Fowler's book, Refactoring, he describes how you can form a template method to eliminate duplicate behavior. I wont get into the motivations for doing such a thing; for that, I suggest buying the book. However, I would like to use the example to show how I would implement it using Ruby.

The prefactored code in Ruby could look like this:
class Customer
def statement
result = "Rental Record for #{name}\n"
_rentals.each do |rental|
result += "\t#{rental.movie.title}\t#{rental.charge}\n"
end
result += "Amount owed is #{total_charge}\n"
result += "You earned #{total_frequent_renter_points} frequent renter points"
end

def html_statement
result = "<h1>Rentals for <em>#{name}</em></h1><p>\n"
_rentals.each do |rental|
result += "#{rental.movie.title}: #{rental.charge}<br>\n"
end
result += "<p>You owe <em>#{total_charge}</em></p>\n"
result += "On this rental you earned <em>#{total_frequent_renter_points}</em> frequent renter points<p>"
end
end
The first refactoring step is to create a Statement base class and two derived classes that inherit from Statement. However, we will need a Statement class and two modules that contain the methods we require.
class Statement
end

module TextStatement
end

module HtmlStatement
end
The next step is to alter Customer to use our new Statement class.
class Customer
def statement
Statement.new.extend(TextStatement).value(self)
end

def html_statement
Statement.new.extend(HtmlStatement).value(self)
end
end

module TextStatement
def value(customer)
result = "Rental Record for #{customer.name}\n"
customer.rentals.each do |rental|
result += "\t#{rental.movie.title}\t#{rental.charge}\n"
end
result += "Amount owed is #{customer.total_charge}\n"
result += "You earned #{customer.total_frequent_renter_points} frequent renter points"
end
end

module HtmlStatment
def value(customer)
result = "<h1>Rentals for <em>#{customer.name}</em></h1><p>\n"
customer.rentals.each do |rental|
result += "#{rental.movie.title}: #{rental.charge}<br>\n"
end
result += "<p>You owe <em>#{customer.total_charge}</em></p>\n"
result += "On this rental you earned <em>#{customer.total_frequent_renter_points}</em> frequent renter points<p>"
end
end
Next, Martin begins to move the differing behavior to separate methods.
module TextStatement
def value(customer)
result = header_string(customer)
customer.rentals.each do |rental|
result += each_rental_string(rental)
end
result += footer_string(customer)
end

def header_string(customer)
"Rental Record for #{customer.name}\n"
end

def each_rental_string(rental)
"\t#{rental.movie.title}\t#{rental.charge}\n"
end

def footer_string(customer)
"Amount owed is #{customer.total_charge}\n" +
"You earned #{customer.total_frequent_renter_points} frequent renter points"
end
end

module HtmlStatement
def value(customer)
result = header_string(customer)
customer.rentals.each do |rental|
result += each_rental_string(rental)
end
result += footer_string(customer)
end

def header_string(customer)
"<h1>Rentals for <em>#{customer.name}</em></h1><p>\n"
end

def each_rental_string(rental)
"#{rental.movie.title}: #{rental.charge}<br>\n"
end

def footer_string(customer)
"<p>You owe <em>#{customer.total_charge}</em></p>\n" +
"On this rental you earned <em>#{customer.total_frequent_renter_points}</em> frequent renter points<p>"
end
end
The final (hopefully obvious) step is to pull up the value method.
class Statement
def value(customer)
result = header_string(customer)
customer.rentals.each do |rental|
result += each_rental_string(rental)
end
result += footer_string(customer)
end
end

module TextStatement
def header_string(customer)
"Rental Record for #{customer.name}\n"
end

def each_rental_string(rental)
"\t#{rental.movie.title}\t#{rental.charge}\n"
end

def footer_string(customer)
"Amount owed is #{customer.total_charge}\n" +
"You earned #{customer.total_frequent_renter_points} frequent renter points"
end
end

module HtmlStatement
def header_string(customer)
"<h1>Rentals for <em>#{customer.name}</em></h1><p>\n"
end

def each_rental_string(rental)
"#{rental.movie.title}: #{rental.charge}<br>\n"
end

def footer_string(customer)
"<p>You owe <em>#{customer.total_charge}</em></p>\n" +
"On this rental you earned <em>#{customer.total_frequent_renter_points}</em> frequent renter points<p>"
end
end
At this point, forming a template method by using extend should be clear. But why use extend instead of inheritance? The answer is you would use extend if the modules you were creating could be used to extend various classes.

For example, let's imagine that the next requirement of our application is to display the above information for only the previous month. The current statement class gives a list of each rental associated with a customer for all months. To satisfy our new requirement we could create a MonthlyStatement class similar to the code below.
MonthlyStatement
def value(customer)
result = header_string(customer)
rentals = customer.rentals.collect { |rental| rental.date > DateTime.now - 30 }
rentals.each do |rental|
result += each_rental_string(rental)
end
end
end
The advantage to the module approach and mixins is now clear: if we had chosen inheritance we would not need to create a TextMonthlyStatement class and a HtmlMonthlyStatement class. However, because we chose to use modules instead of inheritance, we can simply mixin their behavior and achieve reuse without additional classes.
class Customer
def statement
Statement.new.extend(TextStatement).value(self)
end

def html_statement
Statement.new.extend(HtmlStatement).value(self)
end

def monthly_statement
MonthlyStatement.new.extend(TextStatement).value(self)
end

def monthly_html_statement
MonthlyStatement.new.extend(HtmlStatement).value(self)
end
end
Post a Comment