【问题标题】:Java method accepting different functional interface types - possible?Java 方法接受不同的功能接口类型 - 可能吗?
【发布时间】:2021-09-21 20:04:11
【问题描述】:

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

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

    @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...)——这就是它变得难以忍受的地方。

那么,以所有 DRY 的名义:您将如何清理这段代码?我想像T execCall(String x, Class&lt;U&gt; c, SOMETHING, SOMETHING_ELSE) 这样的东西,其中 SOMETHING 可以是 OneArgCall、TwoArgCall... 接口中的任何一个,而 SOMETHING_ELSE 代表参数列表(?)。

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

【问题讨论】:

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

标签: java generics lambda dry functional-interface


【解决方案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 接口。这是使用“部分应用”的函数式编程概念。

【讨论】:

  • 我喜欢它。传统的面向类的 OO 与函数式编程。情节变厚了。
  • @GonenI 它正在使用新的 lambda 功能,但您可以通过使用匿名内部类甚至完全定义的接口轻松实现这个 pre-java8。关键是,你让调用者封装额外的参数,你不需要关心它们。
  • lambdas 并不新鲜,信不信由你,它们已经存在 7 年了
  • @njzk2 哇,时光飞逝!
【解决方案2】:

我可以想到一些受 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;
}

2。使用模板方法模式

将 execCall 放在抽象基类中。在包括 doSomething 和 tryCatch 在内的抽象类中有大部分的 execCall。将 call.Execute 行变成一个抽象方法,该方法被每个派生类覆盖。为 2 args 调用、3 arg 调用等创建了 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);
    }

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

【讨论】:

    【解决方案3】:

    不使用代码生成技巧或重构 API 本身。这两个当然都是选择,尽管我认为我更喜欢第二个。

    代码生成

    它很可能采用这种形式:

    • 您编写了一个注释处理器。这有点用词不当。 AP 作为编译过程的一部分运行。它们通常由注释“触发”并从注释中获取大部分输入参数,但这不是必需的。 “编译器插件”将是您应该如何看待它们。
    • 此注释处理器将处理“模板”类。这个模板类就像你现在存在的 20 个execCall 方法,除了它被命名为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);
    }
    
    • 生成的源文件是您的公共类的源代码,您在此项目中的所有其他代码都将使用它。

    您确实遇到了 AP 的常见缺点:它们会稍微减慢构建过程,如果您正在处理 AP 代码本身,您的代码往往会一团糟(因为您的所有代码现在都在调用在生成之前不存在的方法,这还没有发生并且当前不能发生,因为您的 AP 正在处理中) - 至少在您运行实际构建之前。 Eclipse 倾向于在使这变得简单和快速方面做得很好(只需保存文件,eclipse 会根据需要运行 AP),大多数其他 IDE 将这项工作用于构建,而构建速度往往不会那么快,但是,一旦完成工作,您通常不会在该 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);
    }
    

    好:

    public <T> make(Function<String, T> factory, String param) {
        return factory.apply(param);
    }
    

    你可以在哪里调用它:

    Function<String, QuitMessage> quitMessageFactory = param -> new QuitMessage(param);
    
    make(quitMessageFactory, "Going to sleep for the night");
    

    代替:

    make(QuitMessage.class, "Going to sleep for the night");
    

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

    尝试:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-21
      • 2017-09-21
      • 1970-01-01
      • 2020-08-10
      • 1970-01-01
      • 2017-01-10
      相关资源
      最近更新 更多