The prefactored code in Ruby could look like this:
class CustomerThe 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.
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
class StatementThe next step is to alter Customer to use our new Statement class.
end
module TextStatement
end
module HtmlStatement
end
class CustomerNext, Martin begins to move the differing behavior to separate methods.
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
module TextStatementThe final (hopefully obvious) step is to pull up the value method.
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
class StatementAt 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.
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
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.
MonthlyStatementThe 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.
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
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
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.
ReplyDeleteThanks 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@Jordan,
ReplyDeleteI changed the color, a refresh should take care of the issue. Thanks for pointing that out.
@Dave,
Thanks, I'll check it out.
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* =)
ReplyDeleteI'm using Aurita::GUI for some weeks, for anything related to rendering a HTML-GUI.
ReplyDeletehttp://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.
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