为了扩展StriplingWarrior的答案,我认为以下模式是必要的(这是一个分层流畅构建器API的配方)。
解决方案
首先,需要一个基础抽象类(或接口),它规定了返回扩展该类实例的运行时类型的契约:
abstract class SelfTyped<SELF extends SelfTyped<SELF>> {
abstract SELF self();
}
所有中间扩展类都必须是抽象的
并保持递归类型参数SELF
:
public abstract class MyBaseClass<SELF extends MyBaseClass<SELF>>
extends SelfTyped<SELF> {
MyBaseClass() { }
public SELF baseMethod() {
return self();
}
}
进一步的派生类可以按照同样的方式进行。但是,这些类中没有一个可以直接用作变量类型,除非使用原始类型或通配符(这违背了该模式的目的)。例如(如果
MyClass
不是
abstract
):
//wrong: raw type warning
MyBaseClass mbc = new MyBaseClass().baseMethod();
//wrong: type argument is not within the bounds of SELF
MyBaseClass<MyBaseClass> mbc2 = new MyBaseClass<MyBaseClass>().baseMethod();
//wrong: no way to correctly declare the type, as its parameter is recursive!
MyBaseClass<MyBaseClass<MyBaseClass>> mbc3 =
new MyBaseClass<MyBaseClass<MyBaseClass>>().baseMethod();
这就是我称之为“中间类”的原因,也是为什么它们都应该被标记为
abstract
。 为了闭合循环并利用模式,“叶子”类是必要的,它们使用自己的类型解析继承的类型参数
SELF
并实现
self()
。 它们还应该被标记为
final
以避免违反契约:
public final class MyLeafClass extends MyBaseClass<MyLeafClass> {
@Override
MyLeafClass self() {
return this;
}
public MyLeafClass leafMethod() {
return self();
}
}
这些类使模式更易用:
MyLeafClass mlc = new MyLeafClass().baseMethod().leafMethod();
AnotherLeafClass alc = new AnotherLeafClass().baseMethod().anotherLeafMethod();
这里的价值在于可以在类层次结构中上下链式调用方法,同时保持相同的特定返回类型。
免责声明
以上是Java中奇异递归模板模式的实现。该模式并不绝对安全,应仅保留在内部API的内部工作中使用。原因是在上述示例中,无法保证类型参数SELF
实际上将解析为正确的运行时类型。例如:
public final class EvilLeafClass extends MyBaseClass<AnotherLeafClass> {
@Override
AnotherLeafClass self() {
return getSomeOtherInstanceFromWhoKnowsWhere();
}
}
这个例子揭示了模式中的两个漏洞:
EvilLeafClass
可以“说谎”,并将任何扩展 MyBaseClass
的类型替换为 SELF
。
- 与此无关,
self()
是否实际返回 this
是不确定的,这可能是一个问题或不是问题,这取决于基本逻辑中状态的使用。
由于这些原因,此模式很容易被误用或滥用。为了防止这种情况发生,不允许公开扩展涉及的任何类 - 注意我在 MyBaseClass
中使用包私有构造函数来替换隐式公共构造函数:
MyBaseClass() { }
如果可能的话,也请将
self()
保持为包私有,这样它就不会对公共API造成干扰和混淆。不幸的是,只有在
SelfTyped
是抽象类时才能实现这一点,因为接口方法隐式地为public。
正如zhong.j.yu在评论中
指出的那样,绑定
SELF
的限制可能会被移除,因为它最终无法确保"self类型":
abstract class SelfTyped<SELF> {
abstract SELF self();
}
Yu建议只依赖合同,避免因不直观的递归绑定而产生混淆或虚假安全感。就我个人而言,我更喜欢保留这个绑定,因为SELF extends SelfTyped<SELF>
代表了Java中自类型的最接近的可能表达方式。但是,Yu的意见确实与Comparable
所设定的先例相一致。
结论
这是一种有价值的模式,可以流畅而表达性地调用您的构建器 API。我在一些重要工作中使用过它,特别是编写自定义查询构建器框架,使得调用站点像这样:
List<Foo> foos = QueryBuilder.make(context, Foo.class)
.where()
.equals(DBPaths.from_Foo().to_FooParent().endAt_FooParentId(), parentId)
.or()
.lessThanOrEqual(DBPaths.from_Foo().endAt_StartDate(), now)
.isNull(DBPaths.from_Foo().endAt_PublishedDate())
.or()
.greaterThan(DBPaths.from_Foo().endAt_EndDate(), now)
.endOr()
.or()
.isNull(DBPaths.from_Foo().endAt_EndDate())
.endOr()
.endOr()
.or()
.lessThanOrEqual(DBPaths.from_Foo().endAt_EndDate(), now)
.isNull(DBPaths.from_Foo().endAt_ExpiredDate())
.endOr()
.endWhere()
.havingEvery()
.equals(DBPaths.from_Foo().to_FooChild().endAt_FooChildId(), childId)
.endHaving()
.orderBy(DBPaths.from_Foo().endAt_ExpiredDate(), true)
.limit(50)
.offset(5)
.getResults();
重点是,
QueryBuilder
不仅仅是一个平面实现,而是来自于一系列复杂的构建器类中的“叶子”。同样的模式也被用于像
Where
、
Having
、
Or
等帮助类中,它们都需要共享大量代码。
然而,你不应该忽视最终这只是语法糖。一些有经验的程序员
坚决反对 CRT 模式,或者至少
怀疑其优点是否超过了增加的复杂性。他们的担忧是合理的。
总之,在实现之前,请仔细考虑是否真的有必要,如果确实需要,请不要公开扩展它。