Java 方法接受不同的功能接口类型 - 可能吗?

dan*_*anB 26 java generics lambda dry functional-interface

首先,很抱歉标题不好,但我发现很难用一句话概括我的问题......

我们的软件中有一些代码我非常不满意。事情是这样的:

    @FunctionalInterface
    public interface OneArgCall<T, U, A> {
        T execute(U u, A arg);
    }

    @FunctionalInterface
    public interface TwoArgCall<T, U, A, B> {
        T execute(U u, A arg, B arg2);
    }

    public <T, U, A, B> T execCall(String x, Class<U> c, OneArgCall<T, U, A> call, A arg) {
        U u = doSomething(x, c);
        try {
            return call.execute(u, arg);
        } catch (SomeException se) {
           handleSe(se);
        } catch (SomeOtherException soe) {
           handleSoe(soe);
    }
    
    public <T, U, A, B> T execCall(String x, Class<P> c, TwoArgCall<T, U, A, B> call, A arg, B arg2) {
        U u = doSomething(x, c);
        try {
            return call.execute(u, arg, arg2);
        } catch (SomeException se) {
           handleSe(se);
        } catch (SomeOtherException soe) {
           handleSoe(soe);
    }
Run Code Online (Sandbox Code Playgroud)

即,除了作为第三个参数传递的功能接口(当然还有该接口的参数列表)之外,execCall 方法是相同的。现在,我仍然可以忍受这一点,但是还有更多这样的方法(想象一下 ThreeArgCall、FourArgCall...) - 这就是它变得有点难以忍受的地方。

因此,以 DRY 的名义:您将如何清理这段代码?我想像T execCall(String x, Class<U> c, SOMETHING, SOMETHING_ELSE)SOMETHING 可以是 OneArgCall、TwoArgCall... 接口中的任何一个,而 SOMETHING_ELSE 代表参数列表(?)。

这完全可以做到吗?或者是否有其他方法可以重构此代码以减少重复性?

jta*_*orn 23

您实际上并不需要所有这些接口。您不需要接受任何额外的方法参数。所有这些都可以由调用者使用 lambda 语法来处理。

这是您需要的唯一方法:

public <T, U> T execCall(String x, Class<U> c, Function<U, T> call) {
    U u = doSomething(x, c);
    try {
        return call.apply(u);
    } catch (SomeException se) {
       handleSe(se);
    } catch (SomeOtherException soe) {
       handleSoe(soe);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,假设您有一个需要使用多个参数的方法execCall(),它是如何工作的?

public Foo someMethodCall(Bar bar, Arg1 a1, Arg2 a2) { .... }

Arg1 a1 = ...;
Arg2 a2 = ...;
String x = ...;
Bar b = execCall(x, Bar.class, (u) -> someMethodCall(u, a1, a2));
Run Code Online (Sandbox Code Playgroud)

使用 lambda 语法,您可以将 3 参数方法“调整”为一个 argFunction接口。这是使用“部分应用”的函数式编程概念。

  • @GonenI 它使用新的 lambda 功能,但您可以通过使用匿名内部类甚至完全定义的接口轻松实现此 pre-java8。关键是,你让调用者封装额外的参数,你不需要关心它们。 (8认同)

Gon*_*n I 6

我可以想到一些受 GOF 设计模式启发的方法来减少 execCall 中的重复,但代价是增加了复杂性,并且还给 execCall 的调用者增加了一些负担。

1. 使用适配器或命令模式:

您可以通过让 execCall 仅接受一个接口并将其发送到包装原始接口和参数并委托给它们的 execCall 适配器来删除 execCall 的重复项。这意味着 execCalls 中的代码更少,但其他地方的代码更多。适配器将具有统一的接口,这意味着参数必须包装在统一的基类中,并且特定的适配器必须向下转换为它们知道其实际目标接口所需的参数类型。

您也可以将此解决方案视为实现命令模式,而不是将此解决方案视为适配器。您不必让每个客户端只发送 execCall 的 2 或 3 个参数,而是让它们发送完整的命令对象。这将是一些实现包含抽象执行函数的接口的类。第 2 个参数客户端将向 execCall 发送一个对象,该对象实现带有 arg、arg2 和 call.execute(u, arg) 行的命令接口。原始的 execCall 将执行 commandObj.setU(u),然后调用 commandObj.execute()。这样做的代价是调用者需要了解 call.execute 的知识

interface CallCommand<T,U> 
{
    T execute() ;
    void setU(U u);
}
class  OneArgCallCommand<T,U,A> implements CallCommand<T,U> {
    A arg;
    U u;
    OneArgCall<T, U, A> call;
    public void setU( U u ) { this.u = u; }
    @Override
    public T execute() {
        return call.execute(u, arg);
    }
}
class  TwoArgCallCommand<T,U,A,B> implements CallCommand<T,U> {
    A arg;
    B arg2;
    U u;
    TwoArgCall<T, U, A,B> call;
    public void setU( U u ) { this.u = u; }
    @Override
    public T execute() {
        return call.execute(u, arg,arg2);
    }
}


class Logic
{
public <T, U, A, B> T execCall(String x, Class<U> c, CallCommand<T,U> callCommand) {
    U u = doSomething(x, c);
    callCommand.setU(u);
    try {
        return callCommand.execute();
    } catch (SomeException se) {
       handleSe(se);
    } catch (SomeOtherException soe) {
       handleSoe(soe);
    }
    return null;
}
Run Code Online (Sandbox Code Playgroud)

2. 使用模板方法模式

将 execCall 放入抽象基类中。大部分execCall都在抽象类中,包括doSomething和tryCatch。将 call.Execute 行转换为由每个派生类重写的抽象方法。为 2 个参数调用、3 个参数调用等创建了 execCall 的派生版本。同样,不同的参数需要打包在具有公共抽象参数持有父级的具体类中,并且每个特定的 execCall 需要向下转换为它知道它需要的 arg 类型。这值得么?

3.完全摆脱execCall?

最后,重新思考整个设计可能是值得的。execCall 函数有什么好处?它带来的额外复杂性值得吗?完全避免它可能是可能的,也可能是不可能的。

        try {
    
        // place that originally called execCallWithOneArgument
        call.execute(doSomething(x, c), arg);
    
        // place that originally called execCallWithTwoArguments
        call.execute(doSomething(x, c), arg, arg2);
    
    
        } catch (SomeException se) {
           handleSe(se);
        } catch (SomeOtherException soe) {
           handleSoe(soe);
    }
Run Code Online (Sandbox Code Playgroud)

不确定这是否可行,这取决于代码的结构。


rzw*_*oot 4

并非没有使用代码生成技巧,或者重构 API 本身。这两种当然都是选择,但我认为我更喜欢第二种。

代码生成

它很可能采用这种形式:

  • 您编写一个注释处理器。这有点用词不当。AP 作为编译过程的一部分运行。它们通常由注释“触发”,并从注释中获取大部分输入参数,但这不是必需的。“编译器插件”是你应该如何看待它们的方式。
  • 该注释处理器将处理“模板”类。这个模板类就像execCall现在存在的 20 个方法一样,除了它被命名为ExecCallContainer0ExecCallContainerTemplate,并且是包私有的。当然,还有注释。它只包含一个execCall方法,而不是全部 20 个方法。注释用于“触发”处理器(以便它运行;您还可以将其设计为触发任何内容并检测以Template或不结尾的类)。
  • 注释处理器创建实际的ExecCallContainer类,为您生成所有 20 个变体。这些方法可能只是处理参数(例如将它们收集在列表中或创建一个包装调用的闭包,例如:
/** Generated by AP. Do not edit. */
public <T, U, A, B> T execCall(String x, Class<P> c, TwoArgCall<T, U, A, B> call, A arg, B arg2) {

    Supplier<T> s = () -> call.execute(u, arg, arg2);
    return ExecCallContainerTemplate.exec(x, c, s);
}
Run Code Online (Sandbox Code Playgroud)
  • 生成的源文件是您的公共类的源,该项目中的所有其他代码都将使用该源。

您确实会遇到 AP 的常见缺点:它们会稍微减慢构建过程,如果您正在处理 AP 代码本身,您的代码往往会变得一团糟(因为您的所有代码现在都在调用执行这些操作的方法)在生成它们之前不存在,这还没有发生,并且当前不可能发生,因为您的 AP 正在处理) - 至少在您运行实际构建之前。Eclipse 往往在使此过程变得简单和快速方面做得很好(只需保存文件,Eclipse 就根据需要运行 AP),大多数其他 IDE 将此工作分配给构建,而构建往往不会那么快,但是,一旦完成该 AP 的工作,您通常就不会在该 AP 上进行太多工作,因此这没什么大不了的。

这里最大的复杂性是注释处理器 API 并不完全是微不足道的,所以团队中的某个人可能应该非常熟悉该 API 和这个代码生成器,否则如果其中出现问题,整个团队都会陷入困境——开火模式不太好。任何使用复杂库都会遇到的问题。

重构 API 本身

这段代码中有一些味道。可能它们只是两害相权取其轻,但这表明 API 本身可以简单地进行重构,以便更容易使用和更少的维护 - 双赢。

例如,传递java.lang.Class实例是不好的,而在类型上使用泛型j.l.Class则更糟糕(通常应该是某种工厂接口;你使用实例做什么Class?如果要构造它的实例,你应该有一个工厂代替。如果您将其用作键,则专用键类可能是更好的选择。如果您将其用于反射目的,例如“出于某种原因以编程方式从中获取所有字段”,同样的想法工厂通常是更好的选择,除非您可能不想这样称呼它(“工厂”只是采用类本身的构造函数和元方面并使其可抽象)。

换句话说,不好:

public <T> T make(Class<T> type, String param) {
    Constructor<T> c = type.getConstructor(String.class);
    return c.newInstance(param);
}
Run Code Online (Sandbox Code Playgroud)

好的:

public <T> make(Function<String, T> factory, String param) {
    return factory.apply(param);
}
Run Code Online (Sandbox Code Playgroud)

您可以通过以下方式调用它:

Function<String, QuitMessage> quitMessageFactory = param -> new QuitMessage(param);

make(quitMessageFactory, "Going to sleep for the night");
Run Code Online (Sandbox Code Playgroud)

代替:

make(QuitMessage.class, "Going to sleep for the night");
Run Code Online (Sandbox Code Playgroud)

可能call.execute可以通过外部化传递 xArgsCall 参数和所有参数的工作来抽象。所以,不要有:

public class Calculator {
    public TwoArgCall<Double, Double, Double> addButton = (a, b) -> a + b;

  ....

    public void foo() {
        double lhs = 5.5;
        double rhs = 3.3;
        calculatorTape.execCall(addButton, lhs, rhs);
    }
}
Run Code Online (Sandbox Code Playgroud)

尝试:

public class Calculator {
    public TwoArgCall<Double, Double, Double> addButton = (a, b) -> a + b;

  ....

    public void foo() {
        double lhs = 5.5;
        double rhs = 3.3;
        calculatorTape.execCall(() -> addButton.exec(lhs, rhs));
    }
}
Run Code Online (Sandbox Code Playgroud)

第二个代码段并不比第一个代码段多多少,并且不再需要 20 个execCall方法、20 个XArgsCall功能接口等。我认为这值得任何一天。