Refactoring To Higher-Order Functions

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"))