Ответ 1
Чтобы использовать ответ StriplingWarrior, я думаю, что потребуется следующий шаблон (это рецепт для иерархического свободного API-интерфейса Builder).
Решение
Во-первых, базовый абстрактный класс (или интерфейс), который излагает контракт для возврата типа среды выполнения экземпляра, расширяющего класс:
/**
* @param <SELF> The runtime type of the implementor.
*/
abstract class SelfTyped<SELF extends SelfTyped<SELF>> {
/**
* @return This instance.
*/
abstract SELF self();
}
Все промежуточные классы расширения должны быть abstract
и поддерживать параметр рекурсивного типа SELF
:
public abstract class MyBaseClass<SELF extends MyBaseClass<SELF>>
extends SelfTyped<SELF> {
MyBaseClass() { }
public SELF baseMethod() {
//logic
return self();
}
}
Другие производные классы могут следовать аналогичным образом. Но ни один из этих классов не может использоваться непосредственно как типы переменных, не прибегая к rawtypes или wildcards (который побеждает цель шаблона). Например (если 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() {
//logic
return self(); //could also just return this
}
}
Такие классы делают шаблон применимым:
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
, что может быть или не быть проблемой, в зависимости от использования состояния в базовой логике.
По этим причинам эта модель имеет большой потенциал для злоупотребления или злоупотребления. Чтобы этого не произошло, не разрешайте публично распространять ни один из классов, не обращайте внимание на мое использование конструктора package-private в MyBaseClass
, который заменяет неявный публичный конструктор:
MyBaseClass() { }
Если возможно, сохраните self()
package-private тоже, поэтому он не добавляет шума и путаницы в общедоступный API. К сожалению, это возможно только в том случае, если SelfTyped
является абстрактным классом, поскольку методы интерфейса неявно публичны.
Как отмечает zhong.j.yu в комментариях, ограничение на SELF
может быть просто удалено, поскольку в конечном итоге оно не может обеспечить "тип" ":
abstract class SelfTyped<SELF> {
abstract SELF self();
}
Ю советует полагаться только на контракт и избегать путаницы или ложного чувства безопасности, исходящего из неинтуитивной рекурсивной границы. Лично я предпочитаю оставить границу, так как SELF extends SelfTyped<SELF>
представляет собой максимально возможное выражение типа self в Java. Но мнение Юна определенно связано с прецедентом, установленным 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 или, по крайней мере, скептически относятся к своему выгоды от дополнительной сложности. Их проблемы законны.
Нижняя строка, внимательно изучите, действительно ли это необходимо перед ее внедрением, и если вы это сделаете, не делайте ее общедоступной.