可选地在纯函数中调用副作用

Optionally invoke side effects in a pure function

如何将这个带有副作用的长遗留 Java 代码重构为更纯净的版本?

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

在生产中,所有这些副作用都必须 运行。但是有时我想调用此方法的 "pure" 版本,它看起来像这样:

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

注意:我不想维护两个方法。如果可能的话,我想拥有一个。此外,我希望有时能够有选择地产生一些副作用,比如日志记录,但其他的则没有。重构此代码的最佳方法是什么,以便我可以调用它并有时有副作用,有时没有副作用?

我目前正在使用 Java 8,但我认为这个问题很普遍。到目前为止,我已经想到了两种方法来解决这个问题。首先,我可以向方法传递一个布尔值:"runSideEffects"。如果为 false,则跳过 运行 副作用的代码。另一种更灵活的解决方案是通过要求将 lambda 函数作为参数传递并调用它们而不是调用副作用来改变函数。例如,像 "void log(String msg)" 这样的方法可以作为参数传递。该方法的生产调用可以传递一个将消息写入记录器的函数。其他调用可以传递一个方法,该方法在调用 log(msg) 时实际上什么都不做。这些解决方案都不是很好,这就是我向社区征求建议的原因。

我相信您可以使用像这样的结果变量将它们分开执行,而不是让它单独执行这些步骤 `

public int Steps(int param1,int param2){
//whatever you want your first step to do make result into a variable
int param3 = param1-param2;

//Same with step 2 ,3 and so on
int param4 = param3*param1;

}` 

有可能,但不知何故有点奇怪。

public class MethodPipeline<T, I, R> {
    private final MethodPipeline<T, ?, I> prev;
    private final int kind;
    private final Function<? extends I, ? extends R> f;
    private final Runnable r;
    private final Consumer<? extends R> c;
    private MethodPipeline(Function<? extends I, ? extends R> l, MethodPipeline<? extends T, ?, ? extends I> prev) {
        kind = 0;
        f = l;
        r = null;
        c = null;
        this.prev = prev;
    }
    private MethodPipeline(Runnable l, MethodPipeline<? extends T, ?, ? extends I> prev) {
        kind = 1;
        f = null;
        r = l;
        c = null;
        this.prev = prev;
    }
    private MethodPipeline(Consumer<? extends R> l, MethodPipeline<? extends T, ?, ? extends I> prev) {
        kind = 2;
        f = null;
        r = null;
        c = l;
        this.prev = prev;
    }
    //...various public consructor
    public <R1> MethodPipeline<T, R, R1> then(Function<? extends R, ? extends R1> convertor) {
        return new MethodPipeline<>(convertor, this);
    }
    public MethodPipeline<T, I, R> sideEffect(Runnable sideEffect) {
        return new MethodPipeline<>(sideEffect, this);
    }
    public MethodPipeline<T, I, R> sideEffect(Consumer<? extnds R> sideEffect) {
        return new MethodPipeline<>( sideEffect, this);
    }
    public R run(T param, boolean sideEffect) {
        I v = prev.run(param);
        switch (kind) {
        case 0:
            return f.apply(v);
        case 1:
            if (sideEffect)
                r.run();
            return v;
        case 2:
            if (sideEffect)
                c.accept(v);
            return v;
        }
    }
}

我把它设计成一个管道,就像j.u.stream一样。为了类型安全, run 是递归的。谨慎使用:不要在管道中投入太多工作。它可能会导致 WhosebugException。

PS:网页写的。未经测试。连编译一次都没有。使用风险自负。有界类型变量可能需要一些重构,请自行更改。

一个选项是将方法提取到一个 class 中,每个步骤都有一个空的 template method,并为非纯版本覆盖它:

class Method {
    void beforeStart() {};
    void afterStep1(String result) {};
    void afterStep2(String result) {};
    void afterStep3(String result) {};
    void afterStep4(String result) {};

    final Result execute(String param1, String param2) {
        beforeStart();
        String s1 = step1(param1, param2);
        afterStep1(s1);
        String s2 = step2(s1);
        afterStep2(s2);
        String s3 = step3(s2);
        afterStep3(s3);
        String s4 = step4(s3);
        afterStep4(s4);
        return new Result(s4);
    }
}

然后您可以定义一个或多个子class子元素来覆盖提供的方法以插入副作用。

我不是在宣传这是一个很好的解决方案,而是作为一种讨论您所处情况的问题的方式:

@SafeVarargs
public static Result pureMethod(
    String param1, String param2, Consumer<String>... optionalSteps) {
    if(optionalSteps.length>0) optionalSteps[0].accept(param1);
    String s1 = step1(param1, param2);
    if(optionalSteps.length>1) optionalSteps[1].accept(s1);
    String s2 = step2(s1);
    if(optionalSteps.length>2) optionalSteps[2].accept(s2);
    String s3 = step3(s2);
    if(optionalSteps.length>3) optionalSteps[3].accept(s3);
    String s4 = step4(s3);
    if(optionalSteps.length>4) optionalSteps[4].accept(s4);
    return new Result(s4);
}
public Result nonPureMethod(String param1, String param2) {
  return pureMethod(param1, param2, 
      arg -> this.current_status = "running",
      arg -> this.logger.log("About to do step 2, this could take while"),
      arg -> this.logger.log("Completed step 2"),
      s3  -> { this.notifyOtherObject(s3);
            if (this.UserPressedEmergencyStop) {
                this.current_status = "stopped";
                throw new RuntimeException("stopped");
            }
        },
      s4  -> { this.current_status = "completed"; this.saveFile(s4); });
}

这里引人注目的是这些可选操作的共同点是多么少。上面的代码片段接受了一些动作不会使用提供的参数,并且对于第一个动作,两个参数中的一个是任意选择的。另一种方法是使用 BiConsumer,要求所有其他操作携带未使用的参数。但这里最严重的违规行为是第四步中的异常终止。一个干净的解决方案是使用函数类型 returning a boolean 来确定是否继续,但这会强制所有操作也 return a boolean,例如将像 arg -> this.current_status = "running" 这样的简单 lambda 表达式转换为 arg -> { this.current_status = "running"; return true; },等等

附带说明一下,我不知道您使用的是哪个日志记录框架,但正常记录已经是一个可以通过配置选项转换为无副作用模式的操作。

也许这有助于对您的操作进行分类并创建不同的参数,例如一个Logger,一个状态更新器和一个提前终止谓词,例如

public static Result pureMethod(String param1, String param2,
        Logger logger, ObjIntConsumer<String> statusUpdater, IntPredicate cont) {
    statusUpdater.accept(null, 0);
    String s1 = step1(param1, param2);
    statusUpdater.accept(s1, 1);
    if(!cont.test(1)) return null;
    logger.log("About to do step 2, this could take while");
    String s2 = step2(s1);
    statusUpdater.accept(s2, 2);
    if(!cont.test(2)) return null;
    logger.log("Completed step 2");
    String s3 = step3(s2);
    statusUpdater.accept(s3, 3);
    if(!cont.test(3)) return null;
    String s4 = step4(s3);
    statusUpdater.accept(s4, 4);
    return new Result(s4);
}
public static Result pureMethod(String param1, String param2) {
    Logger logger=Logger.getAnonymousLogger();
    logger.setLevel(Level.OFF);
    return pureMethod(param1, param2, logger, (s,i)->{}, i->true);
}
public Result nonPureMethod(String param1, String param2) {
    return pureMethod(param1, param2, this.logger,
        (s,i)-> { switch (i) {
            case 0: this.current_status = "running"; break;
            case 3: this.notifyOtherObject(s); break;
            case 4: this.current_status = "completed"; this.saveFile(s); break;
        }}, i -> {
            if(i==3 && this.UserPressedEmergencyStop) {
                this.current_status = "stopped";
                return false;
            }
            else return true;
        });
}

但在某些方面它仍然紧贴 nonPureMethod 的用例……

传递函数作为参数。让函数产生副作用。如果你想调用函数的 "pure" 版本,你可以简单地不将副作用函数作为参数传递。

我现在有不同语言版本的 Github 存储库: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;
  }
}