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

6 comments:

  1. Anonymous11:22 AM

    Great article, I'll have to check out that book. BTW, your code text color is very hard to read, especially with the Mac's gamma.

    ReplyDelete
  2. Thanks Jay. Have you read Ola Bini's blog? If not, you should check it out: http://ola-bini.blogspot.com/. Lots of good Ruby meta-programming goodness over there.

    ReplyDelete
  3. Anonymous12:18 PM

    @Jordan,
    I changed the color, a refresh should take care of the issue. Thanks for pointing that out.

    @Dave,
    Thanks, I'll check it out.

    ReplyDelete
  4. Anonymous2:13 AM

    Hi, This is great stuff. I got the link in one of my comments, and we seem to have similar thoughts about Refactoring+Ruby. (*added you to me newsfeed* =)

    ReplyDelete
  5. Anonymous2:13 PM

    I'm using Aurita::GUI for some weeks, for anything related to rendering a HTML-GUI.

    http://rubyforge.org/projects/aurita/

    It has a nice syntax (markaby-like) but provides an object tree so you can change GUI elements after creating them, unlinke with strings.
    It provides a perfect form abstraction, and it's a bliss when needing to implement custom form components, no matter which framework you use.

    ReplyDelete
  6. Anonymous1:25 PM

    Yes, Aurita::GUI is hot. I used it to refactor my views in Rails, and it was damn simple to abstract the whole HTML rendering. And for every complex GUI element, i just define a widget class that's reusable in the whole application. So, i change a widget once, and it's updated everywhere.

    ReplyDelete

Note: Only a member of this blog may post a comment.