我正在编写一个流畅的API来配置和实例化一系列"消息"对象。我有一个消息类型的层次结构。
为了能够在使用流畅的API时访问子类的方法,我使用泛型来参数化子类,并使所有流畅的方法(以"以"开头)返回泛型类型。请注意,我省略了流体方法的大部分主体;其中有很多配置。
public abstract class Message<T extends Message<T>> {
protected Message() {
}
public T withID(String id) {
return (T) this;
}
}
具体子类同样重新定义泛型类型。
public class CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>> {
protected CommandMessage() {
super();
}
public static CommandMessage newMessage() {
return new CommandMessage();
}
public T withCommand(String command) {
return (T) this;
}
}
public class CommandWithParamsMessage extends
CommandMessage<CommandWithParamsMessage> {
public static CommandWithParamsMessage newMessage() {
return new CommandWithParamsMessage();
}
public CommandWithParamsMessage withParameter(String paramName,
String paramValue) {
contents.put(paramName, paramValue);
return this;
}
}
此代码有效,即我可以实例化任何类并使用所有流畅的方法:
CommandWithParamsMessage msg = CommandWithParamsMessage.newMessage()
.withID("do")
.withCommand("doAction")
.withParameter("arg", "value");
以任何顺序调用流畅的方法是这里的主要目标。
但是,编译器会警告所有return (T) this
都不安全。
类型安全:从Message to T
取消选中
我不确定如何重新组织层次结构以使此代码真正安全。尽管它有效,但以这种方式使用仿制药确实令人费解。 特别是,如果我忽略警告,我无法预见会发生运行时异常的情况。 将有新的消息类型,所以我需要保持代码可扩展。 如果解决方案是完全避免继承,我也想获得替代方案的建议。
此处有other questions来解决类似问题。他们指出一个解决方案,其中所有中间类都是抽象的,并声明像protected abstract self()
这样的方法。最后,它还不安全。
答案 0 :(得分:19)
您的代码基本上是对Generics的不安全使用。例如,如果我编写一个扩展消息的新类,请说Threat,并且有一个新方法doSomething(),然后我创建一个由这个新类参数化的消息,它创建一个Message实例,然后尝试转换它到它的子类。但是,由于它是Message的实例,而不是威胁,尝试调用此消息将导致异常。因为Message不能doSOmething()。
此外,这里也没有必要使用泛型。普通的旧继承会很好。由于子类型可以通过使其返回类型更具体来覆盖方法,因此您可以:
public abstract class Message {
protected Message() {
}
public Message withID(String id) {
return this;
}
}
然后
public class CommandMessage extends Message {
protected CommandMessage() {
super();
}
public static CommandMessage newMessage() {
return new CommandMessage();
}
public CommandMessage withCommand(String command) {
return this;
}
}
这可以正常工作,因为您可以按正确的顺序调用参数:
CommandWithParamsMessage.newMessage()
.withID("do")
.withCommand("doAction")
.withParameter("arg", "value");
会失败,但
CommandWithParamsMessage.newMessage().withParameter("arg", "value")
.withCommand("doAction").withID("do")
会成功,因为它只是“up类型”,最后返回一个“消息”类。如果你不想“uptype”,那么只需覆盖继承的命令,现在你可以按任何顺序调用方法,因为它们都返回原始类型。
E.g。
public class CommandWithParamsMessage extends
CommandMessage {
public static CommandWithParamsMessage newMessage() {
return new CommandWithParamsMessage();
}
public CommandWithParamsMessage withParameter(String paramName,
String paramValue) {
contents.put(paramName, paramValue);
return this;
}
@Override
public CommandWithParamsMessage withCommand(String command){
super.withCommand(command);
return this;
}
@Override
public CommandWithParamsMessage withID(String s){
super.withID(s);
return this;
}
}
现在,您将使用上述两个流畅的调用之一流畅地返回CommandWithParamsMessage。
这会解决您的问题,还是我误解了您的意图?
答案 1 :(得分:13)
之前我做过类似的事。它可能变得丑陋。事实上,我尝试过的次数比我用过的次数多;通常它会被删除,我试图找到一个更好的设计。也就是说,为了帮助你在路上走得更远,试试这个:
让您的抽象类声明一个类似的方法:
protected abstract T self();
这可以替换你的return语句中的this
。子类将需要返回与T
的边界匹配的内容 - 但它不保证它们返回相同的对象。
答案 2 :(得分:8)
如果您更改这样的签名,您既不应该收到任何警告,也不需要任何演员表:
abstract class Message<T extends Message<T>> {
public T withID(String id) {
return self();
}
protected abstract T self();
}
abstract class CommandMessage<T extends CommandMessage<T>> extends Message<T> {
public T withCommand(String command) {
// do some work ...
return self();
}
}
class CommandWithParamsMessage extends CommandMessage<CommandWithParamsMessage> {
public static CommandWithParamsMessage newMessage() {
return new CommandWithParamsMessage();
}
public CommandWithParamsMessage withParameter(String paramName, String paramValue) {
// do some work ...
return this;
}
@Override protected CommandWithParamsMessage self() {
return this;
}
}
答案 3 :(得分:4)
编译器警告您这种不安全的操作,因为它无法事实上检查代码的正确性。事实上,这使得它不安全,你无法阻止这种警告。即使不安全的操作没有经过编译检查,它仍然可以在运行时合法。如果您绕过编译器检查,那么您的工作就是验证自己的代码是否使用了正确的类型,这是@SupressWarning("unchecked")
注释的用途。
将此应用于您的示例:
public abstract class Message<T extends Message<T>> {
// ...
@SupressWarning("unchecked")
public T withID(String id) {
return (T) this;
}
}
很好,因为事实上你可以肯定地说这个Message
实例总是由T
表示的类型。但Java编译器不能(还)。与其他抑制警告一样,使用注释的关键是最小化其范围!否则,在进行代码更改后,您可以轻松地保留注释抑制,从而使您以前的手动检查类型安全无效。
由于您只返回this
个实例,因此您可以轻松地将任务外包给另一个答案中建议的特定方法。定义一个protected
方法,如
@SupressWarning("unchecked")
public T self() {
(T) this;
}
你可以像这里一样调用mutator:
public T withID(String id) {
return self();
}
作为另一种选择,如果您可以实现,请考虑一个不可变的构建器,它只通过接口公开其API,但实现完整的构建器。这就是我现在通常建立流畅界面的方式:
interface Two<T> { T make() }
interface One { <S> Two<S> do(S value) }
class MyBuilder<T> implements One, Two<T> {
public static One newInstance() {
return new MyBuilder<Object>(null);
}
private T value; // private constructors omitted
public <S> Two<S> do(S value) {
return new MyBuilder<S>(value);
}
public T make() {
return value;
}
}
当然,您可以创建更智能的结构,避免使用未使用的字段。如果你想看看我使用这种方法的例子,看看我的两个使用流畅接口的项目:
答案 4 :(得分:3)
这不是您原始问题的解决方案。这只是尝试捕捉您的实际意图,并勾画出原始问题未出现的方法。 (我喜欢泛型 - 但像CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>>
这样的类名让我不寒而栗......)
我知道这在结构上与您最初提出的内容有很大不同,您可能在问题中省略了一些细节,这些细节缩小了可能的答案的范围,以便不再使用以下内容适用。
但如果我理解你的意图,你可以考虑让流利的电话处理这些子类型。
这里的想法是,您最初只能 创建一个简单的Message
:
Message m0 = Message.newMessage();
Message m1 = m0.withID("id");
在此消息上,您可以调用withID
方法 - 这是所有消息共有的唯一方法。在这种情况下,withID
方法返回Message
。
到目前为止,该消息既不是CommandMessage
也不是任何其他专门形式。但是,当您调用withCommand
方法时,您显然想要构建CommandMessage
- 所以现在只需返回CommandMessage
:
CommandMessage m2 = m1.withCommand("command");
同样,当您调用withParameter
方法时,您会收到CommandWithParamsMessage
:
CommandWithParamsMessage m3 = m2.withParameter("name", "value");
这个想法大致(!)的灵感来自blog entry,它是德语,但代码很好地展示了如何使用这个概念来构造类型安全的“从哪里选择”查询。
此处,草拟了该方法,大致适合您的用例。当然,有一些细节,实现将取决于实际使用方式 - 但我希望这个想法变得清晰。
import java.util.HashMap;
import java.util.Map;
public class FluentTest
{
public static void main(String[] args)
{
CommandWithParamsMessage msg = Message.newMessage().
withID("do").
withCommand("doAction").
withParameter("arg", "value");
Message m0 = Message.newMessage();
Message m1 = m0.withID("id");
CommandMessage m2 = m1.withCommand("command");
CommandWithParamsMessage m3 = m2.withParameter("name", "value");
CommandWithParamsMessage m4 = m3.withCommand("otherCommand");
CommandWithParamsMessage m5 = m4.withID("otherID");
}
}
class Message
{
protected String id;
protected Map<String, String> contents;
static Message newMessage()
{
return new Message();
}
private Message()
{
contents = new HashMap<>();
}
protected Message(Map<String, String> contents)
{
this.contents = contents;
}
public Message withID(String id)
{
this.id = id;
return this;
}
public CommandMessage withCommand(String command)
{
Map<String, String> newContents = new HashMap<String, String>(contents);
newContents.put("command", command);
return new CommandMessage(newContents);
}
}
class CommandMessage extends Message
{
protected CommandMessage(Map<String, String> contents)
{
super(contents);
}
@Override
public CommandMessage withID(String id)
{
this.id = id;
return this;
}
public CommandWithParamsMessage withParameter(String paramName, String paramValue)
{
Map<String, String> newContents = new HashMap<String, String>(contents);
newContents.put(paramName, paramValue);
return new CommandWithParamsMessage(newContents);
}
}
class CommandWithParamsMessage extends CommandMessage
{
protected CommandWithParamsMessage(Map<String, String> contents)
{
super(contents);
}
@Override
public CommandWithParamsMessage withID(String id)
{
this.id = id;
return this;
}
@Override
public CommandWithParamsMessage withCommand(String command)
{
this.contents.put("command", command);
return this;
}
}