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

首先,很抱歉标题不好,但我发现用一个简短的句子来总结我的问题有点困难......

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

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

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

所以,以所有干燥的名义:你会如何清理这个代码?我想象一些东西,比如某样东西可以是OneArgCall,TwoArgCall中的任何一个......接口和SOMETHING_ELSE表示参数的列表(?)。T execCall(String x, Class<U> c, SOMETHING, SOMETHING_ELSE)

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


答案 1

您实际上并不需要所有这些接口。您无需采用任何额外的方法参数。所有这些都可以由调用方使用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);
    }
}

现在,假设你有一个方法,它接受多个参数,你想用它来使用,这是如何工作的?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));

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


答案 2

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

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

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

与其将此解决方案视为适配器,不如将其视为实现命令模式。您不必让每个客户端只发送其 2 或 3 个参数的 execCall,而是让它们发送一个完整的命令对象。这将是实现包含抽象执行函数的接口的某个类。2 参数客户端将向 execCall 发送一个对象,该对象使用 arg、arg2 和 line 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;
}

2. 使用模板方法模式

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

3.完全摆脱执行呼叫?

最后,重新思考整个设计可能是值得的。执行呼叫功能有什么好处?它是否值得它带来的额外复杂性?完全避免它可能或不可能。

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

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


推荐