In my last post, I demonstrated how we might refactor a simple object oriented piece of code into a functional style with a JavaScript example. The focus of that example was about how to get from instance methods that access mutable fields to stateless functions that use immutable data structures.
I wanted to follow that up with a slightly more sophisticated example to illustrate how we might refactor from an OO design that uses dependency injection to an FP design that uses higher-order functions.
Let’s do it in Ruby this time.
class ResponseWriter | |
def write(customer, serializer, writer) | |
writer.write(serializer.serialize(customer)) | |
end | |
end | |
customer = Customer.new("Mr", "Jason", "Gorman") | |
writer = ResponseWriter.new | |
writer.write(customer, HtmlSerializer.new(), ConsoleWriter.new) | |
writer.write(customer, XmlSerializer.new(), LogFileWriter.new("C:\test\testlog.txt")) | |
writer.write(customer, StringSerializer.new(), NoSqlWriter.new("mongodb", "localhost", "admin", "password123")) |
Here we have a class that writes customer data in a variety of formats – XML, HTML and andstrings – to a variety of output destinations – console, log file and NoSql database.
The serializers all present the same interface, with a write() method that accepts a customer parameter. A good first step might be to pass in lambda that invokes the serialize() method instead of invoking it on the serializer instance inside write().
class ResponseWriter | |
def write(customer, serialize, writer) | |
writer.write(serialize.call(customer)) | |
end | |
end | |
customer = Customer.new("Mr", "Jason", "Gorman") | |
writer = ResponseWriter.new | |
writer.write(customer, lambda {|c| HtmlSerializer.new().serialize(c)}, ConsoleWriter.new) | |
writer.write(customer, lambda {|c| XmlSerializer.new().serialize(c)}, LogFileWriter.new("C:\test\testlog.txt")) | |
writer.write(customer, lambda {|c| StringSerializer.new().serialize(c)}, NoSqlWriter.new("mongodb", "localhost", "admin", "password123")) |
So far, so ugly. Next we can make all our serialize() methods unique.
customer = Customer.new("Mr", "Jason", "Gorman") | |
writer = ResponseWriter.new | |
writer.write(customer, lambda {|c| HtmlSerializer.new().to_html(c)}, ConsoleWriter.new) | |
writer.write(customer, lambda {|c| XmlSerializer.new().to_xml(c)}, LogFileWriter.new("C:\test\testlog.txt")) | |
writer.write(customer, lambda {|c| StringSerializer.new().to_string(c)}, NoSqlWriter.new("mongodb", "localhost", "admin", "password123")) |
Then we can clean things up by turning these instance methods into standalone functions. e.g.
def to_html(customer) | |
return "<table><tr><td>Name</td><td>" + customer.title + " " + customer.first_name + " " + customer.last_name + "</td></tr></table>" | |
end |
…allows us to re-write the client code more cleanly.
writer.write(customer, method(:to_html), ConsoleWriter.new) |
We can rinse and repeat for the output writers. Start by passing in lambdas that invoke their write() methods.
class ResponseWriter | |
def write(customer, serialize, write) | |
write.call(serialize.call(customer)) | |
end | |
end | |
customer = Customer.new("Mr", "Jason", "Gorman") | |
writer = ResponseWriter.new | |
writer.write(customer, method(:to_html), lambda {|o| ConsoleWriter.new().write(o)}) | |
writer.write(customer, method(:to_xml), lambda {|o| LogFileWriter.new("C:\test\testlog.txt").write(o)}) | |
writer.write(customer, method(:to_string), lambda {|o| NoSqlWriter.new( | |
"mongodb", | |
"localhost", | |
"admin", | |
"password123").write(o)}) |
Then make each write() method unique.
customer = Customer.new("Mr", "Jason", "Gorman") | |
writer = ResponseWriter.new | |
writer.write(customer, method(:to_html), lambda {|o| ConsoleWriter.new().write_console(o)}) | |
writer.write(customer, method(:to_xml), lambda {|o| LogFileWriter.new("C:\test\testlog.txt").write_logfile(o)}) | |
writer.write(customer, method(:to_string), lambda {|o| NoSqlWriter.new( | |
"mongodb", | |
"localhost", | |
"admin", | |
"password123").write_nosql(o)}) |
Now, the next part is a little fiddlier. We want to turn these methods into standalone functions. For the console writer, it’s simple because write_console() is stateless, so we don’t have any fields to worry about.
def write_console(output) | |
puts output | |
end |
writer.write(customer, method(:to_html), method(:write_console)) |
But write_logfile() and write_nosql() access fields that are set in constructors. In the previous post, I illustrated how we can refactor from there. All the information those methods need can be passed in as arguments.
class LogFileWriter | |
def write_logfile(output, file_path) | |
# pretend to write string to log file, but this is actually a dummy | |
end |
class NoSqlWriter | |
def write_nosql(output, db_type, url, user_name, password) | |
# pretend to write string to a NoSQL DB, but this is actually a dummy | |
end |
customer = Customer.new("Mr", "Jason", "Gorman") | |
writer = ResponseWriter.new | |
writer.write(customer, method(:to_html), method(:write_console)) | |
writer.write(customer, method(:to_xml), lambda {|o| LogFileWriter.new().write_logfile(o, "C:\test\testlog.txt")}) | |
writer.write(customer, method(:to_string), lambda {|o| NoSqlWriter.new().write_nosql( | |
o, | |
"mongodb", | |
"localhost", | |
"admin", | |
"password123")}) |
Now we can make them standalone functions.
customer = Customer.new("Mr", "Jason", "Gorman") | |
writer = ResponseWriter.new | |
writer.write(customer, method(:to_html), method(:write_console)) | |
write_logfile = lambda {|o| write_logfile(o, "C:\test\testlog.txt")} | |
writer.write(customer, method(:to_xml), write_logfile) | |
write_nosql = lambda {|o| write_nosql( | |
o, | |
"mongodb", | |
"localhost", | |
"admin", | |
"password123")} | |
writer.write(customer, method(:to_string), write_nosql) |
And a final bit of tidying up: if we turn our write_logfile() and write_nosql() into closures, with the outer functions acepting all the messy extra parameters, we can simplify our client code.
def write_nosql(db_type, url, user_name, password) | |
return -> (output) { | |
# pretend to write string to a NoSQL DB, but this is actually a dummy | |
} | |
end |
def write_logfile(file_path) | |
return -> (output) { | |
# pretend to write string to log file, but this is actually a dummy | |
} | |
end |
Last, but not least, we get rid of the ResponseWriter class, making its write() method a standalone function.
def write(customer, serialize, write) | |
write.call(serialize.call(customer)) | |
end | |
customer = Customer.new("Mr", "Jason", "Gorman") | |
write(customer, method(:to_html), method(:write_console)) | |
write(customer, method(:to_xml), write_logfile("C:\test\testlog.txt")) | |
write(customer, method(:to_string), write_nosql("mongodb", | |
"localhost", | |
"admin", | |
"password123")) |