When reading about Functional Programming, I have seen many folks recommend getting rid of if/else statements (See here, here, and here). You can make your code easier to follow and understand by eliminating if/else (or conditional) statements. Doing so will force you to think declaratively. Functional programmers agree that declarative programming is generally easier to understand than imperative programming.
So how do we do this in Java? In my last article, I gave a brief overview of Java's Functional Interfaces and how you can use them to make your code more declarative. Let's find out how we can use them to remove some if/else logic.
I ran into something similar to the code below in a project I was working on. The code has a method that determines which kind of report to generate. Report generation is based on the parameter that is passed into the method. Don't get too focused on the response code. For this exercise, you could replace the response logic with any type of algorithm that could be change based on any runtime decision. Notice that each case does something different (or has a different "strategy") with the response.
// in controller
public void formatResponse(String format, HttpServletResponse response) {
if("csv".equals(format)) {
response.setContentType("text/csv");
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.csv;");
} else if ("html".equals(format))
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.html;");
} else if ("pdf".equals(format)) {
response.setContentType(MediaType.APPLICATION_PDF_VALUE);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.pdf;");
} else {
response.setContentType(MediaType.TEXT_XML);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline:filename=myReport.xml;");
}
}
This method was called in the controller as such:
public void getReport(@PathVariable String reportName, @RequestBody String format, HttpServletResponse response) {
// other code removed for brevity
formatResponse(format, response);
// other code removed for brevity
}
Let me say that multiple patterns and techniques can be used to refactor if/else statements. This includes (but is not limited to) the Command Pattern, the Factory Pattern, Enums, Maps, and the Strategy Pattern. In the following case, I decided on implementing the Strategy Pattern.
A typical strategy pattern would have us create an Interface and then create classes that implement the Interface. Maybe something like this:
public interface ResponseStrategy {
void accept(HttpServletResponse response);
}
public static class CsvStrategy implements ResponseStrategy {
@Override
public void accept(HttpServletResponse response){
response.setContentType("text/csv");
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.csv;");
}
}
public static class HtmlStrategy implements ResponseStrategy {
@Override
public void accept(HttpServletResponse response){
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.html;");
}
}
// other classes ect...
Nothing wrong with that approach. But implementing a new class for every strategy can become cumbersome. So how can Functional Interfaces help us out here?
How can we use Functional Interfaces in the Strategy Pattern?
Functional Interfaces can be implemented by lambda expressions. Sometimes, it makes sense to replace simple classes with anonymous classes. And anonymous classes that only have one method can be replaced by a lambda. In our above case, the classes do not do much. Each of the classes could be refactored into an anonymous class. Since they only implement one method, they could be further refactored into lambdas.
We could make our own functional interface to define our lambdas.
@FunctionalInterface
public interface ResponseStrategy {
void accept(HttpServletResponse response);
}
Or, we could use Java Util's built-in functional interface Consumer. Which states: "[A Consumer] represents an operation that accepts a single input argument and returns no result. Unlike most other functional interfaces, Consumer is expected to operate via side-effects."
Since using Consumer would be one less interface I would have to write, I will use it as follows:
private static Consumer<HttpServletResponse> csvStrategy = response -> {
response.setContentType("text/csv");
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.csv;");
}
private static Consumer<HttpServletResponse> htmlStrategy = response -> {
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.html;");
}
private static Consumer<HttpServletResponse> pdfStrategy = response -> {
response.setContentType(MediaType.APPLICATION_PDF_VALUE);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.pdf;");
}
private static Consumer<HttpServletResponse> xmlStrategy = response -> {
response.setContentType(MediaType.TEXT_XML);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=myReport.xml;");
}
Note: you do not have to pass the lambda's into variables; you can pass the entire function if desired. I did so for readability.
Now we have 4 lambda expressions that we can pass into a map whose key is the required format.
private Map<String, Consumer<HttpServletResponse>> createStrategies() {
Map<String, Consumer<HttpServletResponse>> strategies = new HashMap<>();
strategies.put("csv", csvStrategy);
strategies.put("html", htmlStrategy);
strategies.put("pdf", pdfStrategy);
strategies.put("xml", xmlStrategy);
return Collection.unmodifiableMap(strategies);
}
Since the "else" block in the old code is the default case, we can use a map as follows:
public void formatResponse(String format, HttpServletResponse response) {
Map<String, Consumer<HttpServletResponse>> strategies = createStrategies();
strategies.getOrDefault(format, xmlStrategy).accept(response);
}
Instead of checking a condition and then executing something, the code grabs a Consumer from the map and calls the Consumer's function with the passed-in response. No more if/else logic!
This is a simple example of using Java's Functional Interfaces to help you write cleaner, more concise code. Add a comment if you have utilized anything like this to get rid of if/else statements, or if you have any other cool ways to use Functional Interfaces.
Top comments (1)
Great that you do this series.
A small thing that could cut down on the number of characters to digest is to use var
public void formatResponse(String format, HttpServletResponse response) {
var strategies = createStrategies();
strategies.getOrDefault(format, xmlStrategy).accept(response);
}
Or even inline:
public void formatResponse(String format, HttpServletResponse response) {
createStrategies().getOrDefault(format, xmlStrategy).accept(response);
}
Write something about immutable data and parallell streams!