如何将这种带有副作用的遗留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);
}
在生产中,所有这些副作用都必须运行。但有时候我想调用一个纯粹的"这个方法的版本,看起来像这样:
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)时无效。这些解决方案都不是很好,这就是我向社区提出建议的原因。
答案 0 :(得分:2)
我不会将此作为一个很好的解决方案,但更多的是作为讨论您的情况问题的方式:
@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
,要求所有其他操作携带未使用的参数。但这里最糟糕的罪犯是在第四次行动中通过例外终止。一个干净的解决方案是使用返回boolean
的函数类型来确定是否继续,但这会强制所有操作也返回boolean
,例如将arg -> this.current_status = "running"
之类的简单lambda表达式转换为arg -> { this.current_status = "running"; return true; }
等等。
作为旁注,我不知道您正在使用哪个日志框架,但正常日志记录已经是一个可以通过配置选项转换为无副作用模式的操作。
将行动分类并创建不同的参数可能会有所帮助,例如: a 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
的用例......
答案 1 :(得分:1)
一个选项是将方法提取到每个步骤为空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);
}
}
然后,您可以定义一个或多个子类来覆盖提供的插入副作用的方法。
答案 2 :(得分:0)
将函数作为参数传递。使功能做副作用。如果你想调用一个纯粹的&#34;你可以简单地不将副作用函数作为参数传递。功能的版本。
我现在可以使用不同语言作为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;
}
}
答案 3 :(得分:-1)
我相信你可以而不是让它分别使用结果变量将它们分成一个步骤 `
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;
}`
答案 4 :(得分:-1)
有可能,但有点不确定。
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
是递归的。谨慎使用:不要在管道中投入太多工作。它可能导致StackOverFlowException。