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

view raw
response_writer.rb
hosted with ❤ by GitHub

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

view raw
response_writer.rb
hosted with ❤ by GitHub

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

view raw
response_writer.rb
hosted with ❤ by GitHub

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

view raw
html_serializer.rb
hosted with ❤ by GitHub

…allows us to re-write the client code more cleanly.

writer.write(customer, method(:to_html), ConsoleWriter.new)

view raw
response_writer.rb
hosted with ❤ by GitHub

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

view raw
response_writer.rb
hosted with ❤ by GitHub

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

view raw
response_writer.rb
hosted with ❤ by GitHub

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

view raw
console_writer.rb
hosted with ❤ by GitHub

writer.write(customer, method(:to_html), method(:write_console))

view raw
response_writer.rb
hosted with ❤ by GitHub

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

view raw
log_file_writer.rb
hosted with ❤ by GitHub

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

view raw
no_sql_writer.rb
hosted with ❤ by GitHub

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

view raw
response_writer.rb
hosted with ❤ by GitHub

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)

view raw
response_writer.rb
hosted with ❤ by GitHub

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

view raw
no_sql_writer.rb
hosted with ❤ by GitHub

def write_logfile(file_path)
return -> (output) {
# pretend to write string to log file, but this is actually a dummy
}
end

view raw
log_file_writer.rb
hosted with ❤ by GitHub

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

view raw
response_writer.rb
hosted with ❤ by GitHub

 

 

 

 

 

Author: codemanship

Founder of Codemanship Ltd and code craft coach and trainer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s