如何为我的流畅API构建接口层次结构?

4

我正在开发一个流畅的API,并尝试利用Java的泛型方法来为我的用户提供一个优雅的API,处理类型转换。但由于类型擦除,我在使用中遇到了一些问题。

这是我的接口的简化版本,展示了我遇到的问题:

interface Query<T extends Query<T>> {
  T execute();
  Query<T> appendClause();
}

interface A<T extends A> extends Query<T> { }

class AImpl implements A<A> {
  A execute() { ... }
  Query<A> appendClause() { ... }
}

我在AImpl.appendClause()中遇到了一个错误。编译器显示A不在其范围内,应该扩展Query。据我所知,我的声明AImpl实现了A<A>意味着A确实扩展了Query<A>

根据这里的另一个答案,我尝试通过更改AImpl来分解任何潜在的无法解决的递归:

class AImpl implements A<AImpl> {
  AImpl execute() { ... }
  Query<AImpl> appendClause() { ... }
}

现在我编译A时遇到错误,提示“类型参数T不符合其限制”。有没有人有处理这种情况的建议?Java的泛型让我头疼。

编辑

我已经将A的定义更改为

interface A<T extends A<T>> extends Query<T> { }

这使得 AImpl 的第二个实现工作了。但我还想扩展 API,使其能够查询 A 的子类:

interface B<T extends B> extends A<B> { }

class BImpl implements B<BImpl> {
  BImpl execute() { ... }
  Query<BImpl> appendClause() { ... }
}

这个定义在声明 B 时会出现错误:"类型参数 B 不在其界限内;应该扩展 A"。

我可以通过将 B 更改为以下内容来消除该错误:

interface B<T extends B<T>> extends A<B<T>> { }

但是现在我的接口定义开始看起来很荒谬,我感觉我做错了什么。此外,BImpl仍然出现错误:“BImpl中的appendClause()无法实现Query中的appendClause(); 试图使用不兼容的返回类型”。

有没有建议如何清理子类定义,以便我不需要在extends中指定整个继承层次结构,或者如何使BImpl工作?

编辑2

好的,我又遇到了另一个问题。我有一个生成查询的工厂类:

public class QueryFactory {
  public static <T extends Query<T>> Query<T> queryForType(Class<T> type) { ... }
}

以及客户端代码:

Query<B> bQuery = QueryFactory.queryForType(B.class);

我的客户端代码在声明bQuery时出现错误:“类型参数'B'不在其范围内;应该扩展'Query'。” 我认为此时B已经扩展了Query...

如果我将queryForType()调用更改为以下内容,这个错误就消失了

Query<? extends B> bQuery = QueryFactory.queryForType(B.class);

但是我仍然从编译器得到了未经检查的警告:

unchecked method invocation: <T>queryForType(Class<T>) in QueryFactory is applied to Class<B>
unchecked conversion found: Query required: Query<B>

看起来类型擦除又在跟我作对了,但是我不理解这些警告。还有什么建议可以让我重新回到正确的轨道上吗?谢谢!

编辑3

如果我将客户端代码改为以下内容,则可以编译而无警告:

Query<BImpl> bQuery = QueryFactory.queryForType(BImpl.class);

但是我真的希望隐藏API用户所看不到的实现类。我曾尝试将B改为抽象类而非接口,以防问题出在这方面,但没有帮助。


类型Query<T extends Query<T>>代表什么意思?我认为这个签名没有意义。例如,我希望Query<Apple>返回一个Apple的实例,而不是像execute()那样返回一个Query<Apple>的实例? - Pyranja
在这种情况下,Apple扩展了Query<Apple>.execute(),返回一个Apple对象,您可以在该对象上调用appendClause()方法以查找更具体的Apple。 - Greg
你读过这篇文章了吗?http://www.unquietcode.com/blog/2011/programming/using-generics-to-build-fluent-apis-in-java/ - cmbaxter
相关:https://dev59.com/vGs05IYBdhLWcg3wQvke - Paul Bellora
感谢您的解释@Greg。对于这个有趣的问题,我点赞了。 - Pyranja
1个回答

6

在这里,你的接口A声明了类型参数T,其限定为原始类型A。你应该尝试:

//                      v--- Add this
interface A<T extends A<T>> extends Query<T> { }

这确保了T使用T泛型化的A,这样,T将在Query接口中指定的范围内。如果要实现A<AImpl>接口,则可以使用你的第二个AImpl版本。 编辑 需要说的是:
interface B<T extends B<T>> extends A<B<T>> { }

看起来过于复杂。对于 B接口,就像你从 Query 扩展 A 一样,扩展它:

//                                    v-- Don't mention B here
interface B<T extends B<T>> extends A<T> { }

那么你的 BImpl 类可以类似于你的 AImpl 类:

class BImpl implements B<BImpl> {
    public BImpl execute() { ... }
    public Query<BImpl> appendClause() { ... }
}

好的,这样就解决了A上的错误。但我在尝试对A进行子类化时仍然遇到问题。我会编辑我的问题以反映新的问题。 - Greg
我已经修改了我的答案以回应您修改后的问题。 - rgettman
当我尝试添加一个工厂类来生成查询时,我遇到了另一个问题。现在我已经解决了它,并在另一个答案中记录了解决方案。 - Greg

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