Template Method rocks but Higher Order Function can do the trick
In this short article, I’m going to show an elegant functional solution to a real-world problem and its OOP equivalently elegant solution, from an OOP developer perspective.
At Qualyteam we have lots of scripts related to DevOps. As the number of scripts grew, our team had to reach for npm/docker/kubernetes/build/migration scripts in files, wikis, and documentations spread around. To solve this problem, we’ve built an internal CLI using Node JS (I will write an article about it soon!) to avoid this search hassle and free our talented developers to keep delivering our amazing products.
One of the CLI’s feature is to allow a developer to choose a test spec file to run:
This is the function that shows this option dialog:
We pass a directory as a parameter. At line 2, the
filesystem.list helper returns a string array containing all filenames within that directory. We use this file array as options to a prompt dialog, at line 9. The chosen file is then returned.
After a few days of usage, we’ve come with the idea to let developers choose a Docker image to build. We wanted to use the same function, but the directory containing the Dockerfiles also contained other files not related to Docker, hence the filesystem helper needed to support filtering, returning only .Dockerfile extensions. We’re expecting to just pass a matching parameter to the
filesystem.list helper, but we’ve found out that another helper, the
filesystem.find, was the only function that supported filtering. We could then use a conditional statement to choose between either
filesystem.finddepending on if we want to choose all files or all files matching a blob pattern:
We’ve added an additional matching parameter to
chooseFileand a conditional statement to switch between filesystem helper functions. Conditional statements are evil. They seem to solve everything but they often let the hell lose. We’ve added complexity to our function, and disrespected the Open Closed Principle (OCP):
Software entities should be open for extension, but closed for modification.
There’s no way we could extend the use of this function by adding support for another filesystem helper, and we’ve actually modified it to fulfill the new requirements.
Object Oriented approach
I know how I would solve this problem elegantly with Object Oriented Programming. I would make use of the Template Method design pattern.
Template Method description on Refactoring Guru:
Template Method is a behavioral design pattern that defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.
This is a C# representation of the above
chooseFile function, using the template method pattern:
FileChooser is our superclass class that defines the algorithm’s template. Note that the
Choose is a public method. This is the method that’s going to be called by the client classes. It defines the algorithm template and a step that each subclass should implement, the
Find(Request request) abstract method. Each superclass will override it to implement this method, using the right Filesystem helper:
We’ve managed to solve the problem without a conditional statement! Both finders just override the file fetching algorithm step. Notice how none finders define a public method. The client classes would actually call the abstract class public
Choose method, which would call the subclasses
Find algorithm step. This is called The Holywood Principle:
Don’t call us. We’ll call you
Back to the functional land
Higher Order Function to the rescue
reduceare examples of Higher Order Functions.
filter, for example. It can filter an array given any predicate, and we don’t use inheritance for that. We just pass our predicate, which is a function that returns a boolean, as a parameter, and the
filter function will iterate over the array to return only the values that fulfills our predicate.
But how could we use a Higher Order Function to solve our problem? We can pass the
filesystem helpers result as a function to the
chooseFile function. Remember these conditionals?
Let’s split them into two functions:
Now let’s turn our old
chooseFile into a Higher Order Function:
Now our chooseFile function receives a finder function as a parameter and returns another function. It doesn’t know the finder fetches the files within a folder and is open for extension since it allows any finder to be passed in.
To those unfamiliar with ES6 arrow function syntax, this is the same as:
To avoid having to pass the finder every time you need to choose a file, we can leverage function composition:
Here’s the whole solution:
Our functional solution using a Higher Order Function achieves the same as our OOP solution using the Template Method design pattern. It’s open for extension and closed for modification, since it accepts any finder, and has a single responsibility, since it will not change if a finder requirement changes.