Search code examples
javalambdajava-8refactoringpurely-functional

Optionally invoke side effects in a pure function


How can I refactor this long method of legacy Java code, loaded with side effects, into a more pure version?

public Result nonPureMethod(String param1, String param2){
  this.current_status = "running";
  String s1 = step1(param1, param2);
  this.logger.log("About to do step 2, this could take while");
  String s2 = step2(s1);
  this.logger.log("Completed step 2");
  String s3 = step3(s2);
  this.notifyOtherObject(s3);
  if (this.UserPressedEmergencyStop){ this.current_status = "stopped"; return; }
  String s4 = step4(s3);
  this.current_status = "completed";
  this.saveFile(s4);
  return new Result(s4);
}

In production, all of those side effects have to run. However sometimes I want to invoke a "pure" version of this method, which would look something like this:

public static Result pureMethod(String param1, String param2){
  String s1 = step1(param1, param2);
  String s2 = step2(s1);
  String s3 = step3(s2);
  String s4 = step4(s3);
  return new Result(s4);
}

Notes: I don't want to maintain two methods. If possible I'd like to have one. Also, I'd like to be able to optionally have some of the side effects sometimes, like logging, but not the others. What's the best way to refactor this code, so that I can call it and optionally have the side effects sometimes, and other times not?

I'm currently using Java 8, but I think this problem is pretty general. I've thought of two approaches so far to solve the problem. First, I could pass a boolean to the method: "runSideEffects". If false, just skip the code which runs the side effects. An alternative and more flexible solution would be to alter the function by requiring lambda functions passed as parameters, and calling them instead of invoking side effects. For example, a method like "void log(String msg)" could be passed as a parameter. The production invocation of the method can pass a function which will write the message to the logger. Other invocations can pass a method which effectively does nothing when log(msg) is called. Neither of these solutions feel great, which is why I'm asking the community for suggestions.


Solution

  • Pass functions as parameters. Make the functions do the side effects. You can simply not pass the side-effect functions as parameters if you want to invoke a "pure" version of the function.

    I now have this available in different languages as a Github repository: https://github.com/daveroberts/sideeffects

    package foo;
    
    import java.util.function.BiConsumer;
    import java.util.function.BooleanSupplier;
    import java.util.function.Consumer;
    
    public class SideEffects{
      public static void main(String args[]){
        System.out.println("Calling logic as a pure function");
        String result = logic("param1", "param2", null, null, null, null, null);
        System.out.println("Result is "+result);
        System.out.println();
    
        System.out.println("Calling logic as a regular function");
        result = logic("param1", "param2",
            (level,msg)->{System.out.println("LOG ["+level+"]["+msg+"]");},
            (status)->{System.out.println("Current status set to: "+status); },
            (obj)->{System.out.println("Called notify message on object: "+obj.toString());},
            ()->{boolean dbLookupResult = false; return dbLookupResult;},
            (info)->{System.out.println("Info written to file [["+info+"]]");}
            );
        System.out.println("Result is "+result);
      }
    
      public static String logic(String param1, String param2,
          BiConsumer<String, String> log,
          Consumer<String> setStatus,
          Consumer<Object> notify,
          BooleanSupplier eStop,
          Consumer<String> saveFile){
      if (setStatus != null){ setStatus.accept("running"); }
      String s1 = param1+"::"+param2;
      if (log != null){ log.accept("INFO", "About to do Step 2, this could take awhile"); }
      String s2 = s1+"::step2";
      if (log != null){ log.accept("INFO", "Completed step 2"); }
      String s3 = s2+"::step3";
      if (notify != null) { notify.accept("randomobjectnotify"); }
      if (eStop != null && eStop.getAsBoolean()){
        if (setStatus != null){ setStatus.accept("stopped"); }
        return "stoppedresult";
      }
      String s4 = s3+"::step4";
      if (setStatus != null){ setStatus.accept("completed"); }
      if (saveFile!= null){ saveFile.accept("Logic completed for params "+param1+"::"+param2); }
      return s4;
      }
    }