Search code examples
javaif-statementfunctional-programmingrefactoringfunctional-interface

How to apply DRY principle with Functions in Java?


In a JSF application I have some methods with multiple return points as the following:

public String Method1() {
 ...
 if (condition1) {
   elaborate1();
   return "Case1";
 }
 if (condition2) {
   elaborate2();
   return "Case2";
 }
 if (condition3) {
   elaborate3();
   return "Case3";
 }
 ...
}

The conditions and consequent elaborations are common to more methods, and I would like to write them only once. Unfortunately, it is not easy to refactor due to the multiple return points.

I'm not comfortable with functional interfaces, but I hope I could use them to refactor the code above. My idea is to write something like this:

String elaboration1 ( T NextElaborations) {
 if (condition1) {
   elaborate1();
   return "Case1";
 }
 return NextElaborations.get();
}

Is it possible ? I'm stuck, most examples on internet use lambdas and not methods.

Update 1 @Alexander Ivanchenko I need to study the links you provided before evaluating your answer, I'm not familiar with streams.

After posting the question I made some practice with Functional interfaces and ended up with the following pseudo java code:

@FunctionalInterface
public interface MethodComposition {
   String Execute(Supplier<String> supplier)
}
...
String FinalStep() {
  FinalElaboration();
  return "finalResult";
}

String ElaborationX(Supplier<String> supplier) {
  if (conditionX) {
    elaborationX();
    return "ResultX";
  }
  return supplier.get();
}

...
 
public String Method1() {
   Supplier<String> finalSupplier = this::FinalStep;
   MethodComposition stepX = this::ElaborationX;
   ...
   return step1.Execute(() -> 
          step2.Execute(() ->
          ...
          stepN.Execute(finalSupplier))); 
   }

Despite the ugly syntax it is quite close to what I wanted, I would need more flexibility because ElaborationX() is very similar but not equal in Method1 and Method_N.


Solution

  • You can define a collection of functions representing these conditions and encapsulate this collection inside a class by providing convenient ways of interacting with it. That will make this logic reusable.

    For example, you can create a map that associates a Predicate, which is a function that represents a condition that resolves to boolean value, with a particular return value. Collection should have a predictable order of iteration to insure the order in which conditions will be applied, therefore you can use LinkedHashMap to impose the order of iteration.

    And you can achieve more flexibility by representing a separate condition as an object, containing a predicate some additional attributes. That will allow to categorize conditions and construct different combinations of predicates.

    Your method Method1() with a chain of if-statements can be substituted with a stream of map entries. Or if you fill uncomfortable with streams, you might use a plain for-loop in place of it. The key point is that you need a mean to perform the iteration over the collection of functions.

    I'm not comfortable with functional interfaces, but ...

    I'm stuck, most examples on internet use lambdas and not methods.

    No need to spawn a number of methods like elaboration1(), elaboration2(), etc. that depend on one another, this approach is unflexible. And this approach has nothing to do with functional interfaces in Java (that you've mentioned) and functional approach programming in general. In want to refactor you code to be functional in stile, there's no other way than to actually get familiar with functional programming.

    I encourage you to have a look at these official tutorials on lambda expressions and streams.

    Description of the stream-logic:

    The implementation might look like this:

    public class FunctionStorage {
        
        public static final Map<Predicate<String>, String> conditions = new LinkedHashMap<>(); // LinkedHashMap insures the order of iteration
            
        static {
            conditions.put(s -> false, "1");
            conditions.put(s -> true,  "2");
            conditions.put(s -> false, "3");
        }
        
        public static Stream<Map.Entry<Predicate<String>, String>> getConditions() {
            return conditions.entrySet().stream();
        }  
    }
    

    main()

    public static void main(String[] args) {
        String result = FunctionStorage.getConditions()        // a stream of entries
            .filter(entry -> entry.getKey().test("someValue")) // filtering the entry containing the matching condition
            .findFirst()
            .map(Map.Entry::getValue)                          // extracting the return value
            .orElse("defaultValue"); // orElseThrow() depending on your needs
    }