使用编译器技巧防止方法调用

3

我大致有以下这些类型:

interface Record {}
interface UpdatableRecord extends Record {}

interface Insert<R extends Record> {

    // Calling this method only makes sense if <R extends UpdatableRecord>
    void onDuplicateKeyUpdate();
}

在调用onDuplicateKeyUpdate()时,我希望对<R>有一个额外的限制。这是因为当<R>绑定到任何UpdatableRecord子类型时,此方法才有意义,而不仅仅是Record。例如:

Insert<?>               i1;
Insert<Record>          i2;
Insert<UpdatableRecord> i3;

// these shouldn't compile
i1.onDuplicateKeyUpdate();
i2.onDuplicateKeyUpdate();

// this should compile
i3.onDuplicateKeyUpdate();

有没有什么技巧或方法可以在一个方法声明中为类的泛型类型添加额外的限制?
注意:
- 声明`Insert`不是一个选项,因为我想要一个`Insert`用于那些不可更新的记录。 - 可能会派生`UpdatableInsert extends Insert`并将方法下推,但我不想引入新类型。我有几个这样的方法,带有几个不同的限制,这会导致类型数量爆炸。 - 抛出`UnsupportedOperationException`显然是一种解决方式,但它似乎是使用Java 1.4的方法。我真的很想知道是否可以使用泛型和编译器来解决这个问题。
理想解决方案:
// This would be an annotation to be interpreted by the compiler. There is no such
// thing in Java, as far as I know. But maybe there's a trick having the same effect?
@Require(R extends UpdatableRecord)
void onDuplicateKeyUpdate();

3
Insert 接口中的 onDupliceteKeyUpdate() 方法移除,然后创建一个新接口 UpdatableInsert<R extends UpdatableRecord> extends Insert<R>,在该接口中重新定义 void onDuplicateKeyUpdate(); 方法。这样做是否可行? - Howard
为什么不直接声明接口Insert为<R extends UpdateableRecord>?如果这不可能,那么除非你做类似Howard建议的事情,否则你所要求的是不可能的。 - dontocsata
谢谢您的提示。我会更新我的问题。 - Lukas Eder
2个回答

4
如果你在接口中定义了它,那么任何想要实现该接口的类都必须实现它。因此,你无法绕过在实现Insert<R extends Record>接口的任何类中实现onDuplicateKeyUpdate
你已经在接口中指定了R extends Record,因此这成为了合同的一部分,这意味着可以使用Record的任何子类型。我想不出一种使用泛型添加附加约束并将其限制为仅为UpdatableRecord类型的方法。
此外,你不能根据R的类型在接口中做出决策,因为由于类型擦除,该信息会消失。
对我来说,你的问题似乎围绕RecordUpdatableRecord之间的区别。由于接口是通用契约,我认为不应该在接口中包含特定于类型的行为。因此,你必须找到另一种解决区别的方法。我的解决方案是在Record接口中实现一个返回boolean的方法canUpdate,你可以在onDuplicateKeyUpdate中使用它。这样,如果记录不支持该操作,你就可以抛出UnsupportedOperationException,或者什么也不做(如果你想避免异常,并且在业务逻辑的上下文中什么也不做是有意义的)。
这是我脑海中的一个解决方案;可能有更好的解决方案。我会再考虑一下。
编辑
我仔细思考了一下,是你最新的编辑让我思考了这个问题。 <R extends Record>适用于整个类。因此,在特定方法的上下文中无法覆盖或缩小它。像你提到的这样的注释需要在编译器级别实现,并且在那个级别,我甚至不确定是否可以访问R的类型(因为由于类型擦除)。所以你说的可能是不可能的。
从概念上讲,你要求的不应该在我的意见中是可能的,因为你在合同内部指定了对合同的例外情况。它还听起来像是实现细节渗透到了接口中,因为接口不应该试图弄清楚某些东西能否做到或不能做到;这应该在具体实现中决定。

感谢您的输入。我不需要添加任何额外的方法或检查来能够抛出“UnsupportedOperationExceptions”。这是最简单的方法,再加上一些Javadoc文档说明即可。但我想也许有一种在编译器(因此更安全)级别上实现它的方法。 - Lukas Eder

1
Vivin对你创建Insert时所订立的合同提出了一个很好的观点。我会质疑在那种情境下是否真的有必要使用onDuplicateKeyUpdate()。它似乎只适用于Record的一个子类型。
如果你可以将这个功能整合到另一个Insert方法中(比如说execute()),你可以在那里测试对象的类型并做出相应的行动。例如,
[pseudo-code]
method execute( R record )
 try
  insertRecord( record )
 catch ( KeyAlreadyExistsException e )
  if ( record instanceof UpdatableRecord )
   updateRecord( record )
  end if
 end try
end method

如果你正在寻找更面向对象的方法,你可以将插入逻辑推到Record子类中。在那里,我会将Insert更改为Insertable,给它一个insert()方法,并让Record实现Insertable。然后,Record的每个子类都可以知道如何正确地insert()自己。不过,我不确定这是否符合你的设计策略...


谢谢澄清,这确实让问题的框架有所不同。所以听起来onDuplicateKeyUpdate()是在存在重复键时由外部软件调用插入操作吗?这本质上是一个回调方法? - Mike M
是的,外部软件调用了那个方法。它远不止于此。您实际上可以将 SQL 建模为 Java,就像在 C# 中使用嵌入式 DSL(类似于 LINQ to SQL)。在此处找到一些示例:https://sourceforge.net/apps/trac/jooq/wiki/Manual/DSL/SELECT - Lukas Eder
你说对了!我目前正在抛出一个已检查异常,但我更喜欢在编译时进行检查。编译器拥有所有必要的信息。我只是不知道是否有什么技巧(注意:技巧。我认为Java并不是为这些事情而设计的),让编译器正式检查这些内容。 - Lukas Eder
很遗憾,我对Java编译器或注释的了解不够,无法提供太多帮助。如果您正在寻找的确实是可能的内容,我认为您在注释方面走在了正确的轨道上。最后一个问题浮现在脑海中:在您的情况下,C#版本是否会导致编译错误?如果是这样,他们使用什么技术?请记住,用户可能并不真正期望出现编译错误,因为他们直到运行时才会知道SQL错误。很抱歉我不能提供更多帮助。我一定会关注这个话题,看看是否有解决方案。 - Mike M
@Mike,别担心。这是一个棘手的问题,我预计答案将是不支持。我对C#了解不多。LINQ to SQL本身不支持这样高级的SQL构造。 - Lukas Eder
显示剩余2条评论

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