通用接口

54

假设我想定义一个接口,表示对远程服务的调用。现在,对于远程服务的调用通常会返回某些内容,但也可能包括输入参数。假设实现类通常只实现一个服务方法。鉴于上述信息,以下设计是否不佳(感觉不太对):

public interface IExecutesService<A,B>
{
    public A executeService();
    public A executeService(B inputParameter);
}

现在,假设我用一个类来实现这个接口,并使用一个输入参数执行一个远程服务:

public class ServiceA implements IExecutesService<String,String>
{
  public String executeService()
  {
    //This service call should not be executed by this class
    throw new IllegalStateException("This method should not be called for this class...blabla");
  }

  public String executeService(String inputParameter)
  {
    //execute some service
  }

我有两个关于上述内容的问题:

  1. 在想要为接口方法提供不同输入参数和返回类型的子类的情况下,使用通用接口 (IExecutesService<A,B>) 是否是一个好选择?
  2. 如何更好地实现上述功能?即,我想将我的服务执行器分组到一个公共接口 (IExecutesService) 下;但是,实现类通常只会实现其中一种方法,而使用 IllegalStateException 感觉非常丑陋。此外,在没有任何输入参数调用服务的实现类中,IExecutesService<A,B> 中的 B 类型参数将是多余的。似乎创建两个不同的接口来处理这两种不同的服务调用也过于浪费。

@KenHilton请仅从属于离题/应关闭的问题中删除burninating标签;此问题基于观点,因此应该关闭。当burnination工作完成时,标签将通过系统命令批量删除,但是预先删除标签会损害我们在关键的清理阶段跟踪和查找离题问题的能力。 - TylerH
@KenHilton 如果您对此过程或具体帖子有任何疑问,欢迎加入我们的 burnination 监测聊天室 https://chat.stackoverflow.com/rooms/182583/burnination-progress-for-the-design-tag - TylerH
5个回答

83

这里有一个建议:

public interface Service<T,U> {
    T executeService(U... args);
}

public class MyService implements Service<String, Integer> {
    @Override
    public String executeService(Integer... args) {
        // do stuff
        return null;
    }
}

由于类型擦除,任何类只能实现其中一个。这至少消除了冗余方法。
你提出的接口并不是不合理的,但我不确定它添加了什么价值。您可能只想使用标准的Callable接口。它不支持参数,但该接口的这一部分价值最小(在我看来)。

14
在Java代码中,给接口加上"I"前缀是一种真正的.NET风格,它并不适用于Java代码。 - cletus
4
在.NET出现之前,匈牙利命名法被用于开发Eclipse(以及其祖先VAME等)的代码中,在苏黎世使用I作为接口名称的前缀,因此今天在Eclipse中仍然沿用这种命名方式。 - Yann-Gaël Guéhéneuc
3
具有讽刺意味的是,在接口名称前缀中使用“I”并不真正符合Hungarian Notation。该约定旨在促进关于预期用途的有用信息。例如,“csPassword”可能表示“密码具有加密安全性”,而“rLast”可能表示“最后一行”。添加“I”前缀不提供有关如何使用接口的信息,因此是多余的。 - Dave Jarvis
在这里使用可变参数似乎使设计和解决方案更加可行,我认为这是最好的设计。 - Amos Kosgei

20
这里有另一个建议:
public interface Service<T> {
   T execute();
}

使用这个简单的界面,你可以通过构造函数在具体的服务类中传递参数:

public class FooService implements Service<String> {

    private final String input1;
    private final int input2;

    public FooService(String input1, int input2) {
       this.input1 = input1;
       this.input2 = input2;
    }

    @Override
    public String execute() {
        return String.format("'%s%d'", input1, input2);
    }
}

考虑到接口的远程性质,我认为您需要进行两次远程调用。这可能会在性能方面造成一些代价 :-( - KLE
为什么要进行两次调用?你应该知道你正在调用/创建的是什么(或者服务工厂应该知道),否则,你怎么能调用它呢? - Chii
嗨dfa,能否帮我看看这个问题:https://dev59.com/9azka4cB1Zd3GeqP7mAn - Dipendra Sharma

9

我建议保留两个不同的接口。

你说过:“我想将我的服务执行者分组到一个共同的接口下...创建两个不同的接口似乎有点过度设计...一个类只会实现其中一个接口”

那么,为什么要使用单一接口并不是很清楚。如果您想将其用作标记,可以利用注释来代替。

另一个问题是,您的需求可能会发生变化,并出现具有其他签名的方法。当然,可以使用适配器模式,但是如果特定类实现了带有三个方法的接口(其中两个方法引发UnsupportedOperationException),这将是相当奇怪的。可能会出现第四种方法等。


1
这是唯一正确的答案。您的两组服务通过具有不同签名的方法进行访问。它们不共享接口。就是这么简单。 - Tom Anderson
@user192585:你的问题看起来像是缺乏想象力,未能为两个不同的服务命名。如果你继续将两个不同组的服务放入同一个接口中,最终可能会出现这样一种情况:你会有一组对象由这个唯一的接口表示,但其中一些对象将被允许使用一个服务,而另一些对象则将被允许使用另一个服务,而且并不知道每个对象在集合中属于哪个服务。这是代码混乱的一个很好的例子。 - SylvainL
还有,SOLID原则中的“I”是什么意思? :) - mmmm

4
作为与您的问题严格相关的答案,我支持cleytus的建议。
您也可以使用标记接口(没有方法),例如DistantCall,并具有几个具有所需准确签名的子接口。
  • 一般接口将用于标记所有这些接口,以防您想编写针对所有接口的通用代码。
  • 使用cleytus的通用签名可以减少特定接口的数量。
“可重复使用”接口示例:
    public interface DistantCall {
    }

    public interface TUDistantCall<T,U> extends DistantCall {
      T execute(U... us);
    }

    public interface UDistantCall<U> extends DistantCall {
      void execute(U... us);
    }

    public interface TDistantCall<T> extends DistantCall {
      T execute();
    }

    public interface TUVDistantCall<T, U, V> extends DistantCall {
      T execute(U u, V... vs);
    }
    ....

更新,回应OP的评论

我并没有考虑调用中的任何instanceof。我想你的调用代码知道它在调用什么,并且你只需要为一些通用代码(例如,为了性能原因审计所有远程调用)组装几个远程调用到一个公共接口。 在你的问题中,我没有看到提到调用代码是通用的:-(

如果是这样,我建议你只有一个接口,只有一个签名。多个将只会带来更多的复杂性,却毫无意义。

然而,你需要问自己一些更广泛的问题:
你将如何确保调用方和被调用方正确通信?

这可能是这个问题的后续,或者是一个不同的问题...


我可以看出这个方案会起作用,但是很可能导致代码需要检查实际使用的实例以调用相应的方法(例如xxx instanceof yyy)... 这种类型的编程是否是代码异味?它似乎不是很好的面向对象编程实践。有人能否阐明一下?谢谢。 - user192585

3
如果我理解正确,您希望一个类实现多个具有不同输入/输出参数的接口?这在Java中不起作用,因为泛型是通过擦除实现的。
Java泛型的问题在于,泛型实际上只是编译器魔术。在运行时,类不会保留有关用于通用内容(类类型参数、方法类型参数、接口类型参数)的类型的任何信息。因此,即使您可以具有特定方法的重载,也不能将其绑定到仅在其通用类型参数中不同的多个接口实现上。
总的来说,我能看出您为什么认为这段代码有味道。但是,为了向您提供更好的解决方案,需要了解一些有关您的要求的更多信息。首先,为什么要使用通用接口?

仅为澄清,类不完全不保留任何信息。您仍然可以通过反射检查参数的边界(即如何定义通用类/方法)。但是对象不行,因此您无法确定实例化对象的方式(使用哪些参数)。 - falstro
1
嗨,我不想有一个类实现多个接口。一个类只会实现其中一个接口 - 问题在于一个类通常只会实现接口中的一个方法,使得另一个方法变得多余...这看起来很丑陋。我选择在接口中使用泛型的原因是子类将根据它们实现的服务调用具有不同的返回和输入参数类型。这样清楚了吗?谢谢 :) - user192585
@roe,类的边界(约束)确实存在,但这并不能提供有关特定实例(对象)中有效使用的类型的任何信息。我的感觉是,在Java中使用泛型的许多人都没有意识到这一点。 - Lucero

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接