DEV Community

Cover image for Functional Programming in Java? Refactoring if/else logic with the help of Functional Interfaces.
Perry H
Perry H

Posted on • Updated on

Functional Programming in Java? Refactoring if/else logic with the help of Functional Interfaces.

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;");
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;");
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
perty profile image
Per Lundholm

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!