Шаблон Builder для иерархии полиморфных объектов: возможно с Java?
У меня есть иерархия интерфейсов, с Child
реализация Parent
. Я хотел бы работать с неизменяемыми объектами, поэтому я хотел бы создать классы Builder
, которые бы удобно строили эти объекты. Однако у меня много интерфейсов Child
, и я не хочу повторять код для построения Parent
в каждом типе дочернего построителя.
Итак, предположим следующие определения:
public interface Parent {
public Long getParentProperty();
}
public interface Child1 extends Parent {
public Integer getChild1Property();
}
public interface Child2 extends Parent {
public String getChild2PropertyA();
public Object getChild2PropertyB();
}
Как я могу эффективно реализовать сборщики Child1Builder
и Child2Builder
? Они должны поддерживать работу, например:
Child1 child1 = Child1Builder.newChild1().withChild1Property(5).withParentProperty(10L);
и
Child2 child2 = Child2Builder.newChild2().withChild2PropertyA("Hello").withParentProperty(10L).withChild2PropertyB(new Object());
Я не хочу реализовывать специальный случай withParentProperty
для каждого дочернего построителя.
Отредактировано, чтобы добавить второе свойство в Child2
, чтобы уточнить, что это невозможно сделать с помощью простых дженериков. Я не ищу способ комбинировать Child1
и Child2
- Я ищу способ реализовать систему Builder
, которая не дублирует работу по созданию родительского класса для каждого дочернего класса.
Спасибо за любую помощь!
Ответы
Ответ 1
Решение, которое я предполагаю, похоже на Любопытно повторяющийся шаблон шаблона или CRTP. Вы можете определить базовый класс для обработки инициализации, связанной с родителем, но вы все равно можете обнаружить, что два шаблонных метода getParent()
и getThis()
слишком много повторяются в каждом производном классе связанных дочерних элементов.
Посмотрите:
abstract class ParentBase implements Parent
{
@Override
public final Long getParentProperty()
{
return parentProperty_;
}
protected void setParentProperty(Long value)
{
parentProperty_ = value;
}
private Long parentProperty_;
}
abstract class ParentBuilder<T extends ParentBuilder<T>>
{
T withParentProperty(Long value)
{
getParent().setParentProperty(value);
return getThis();
}
protected abstract ParentBase getParent();
protected abstract T getThis();
}
final class ConcreteChild1 extends ParentBase implements Child1
{
@Override
public Integer getChild1Property()
{
return childProperty_;
}
public void setChild1Property(Integer value)
{
childProperty_ = value;
}
private Integer childProperty_;
}
final class Child1Builder extends ParentBuilder<Child1Builder>
{
public Child1Builder()
{
pending_ = new ConcreteChild1();
}
public Child1Builder withChild1Property(Integer value)
{
pending_.setChild1Property(value);
return this;
}
@Override
protected ParentBase getParent()
{
return pending_;
}
@Override
protected Child1Builder getThis()
{
return this;
}
private final ConcreteChild1 pending_;
}
Как вы можете видеть, тип ParentBuilder
предполагает сотрудничество с производным типом, чтобы он мог возвращать правильно типизированный экземпляр. Его собственная ссылка this
не будет вызвана, потому что тип this
внутри ParentBuilder
, конечно, ParentBuilder
, а не, скажем, Child1Builder
, как предполагалось, для поддержания "плавной" цепочки вызовов.
Я обязал трюк getThis()
учебника Angelika Langer.
Ответ 2
Я не думаю, что getParent()
и getThis()
необходимы, если вы согласны с тем, что методы withXXXProperty()
должны быть вызваны от "младшего" до "самого старого":
class Parent
{
private final long parentProperty;
public long getParentProperty()
{
return parentProperty;
}
public static abstract class Builder<T extends Parent>
{
private long parentProperty;
public Builder<T> withParentProperty( long parentProperty )
{
this.parentProperty = parentProperty;
return this;
}
public abstract T build();
}
public static Builder<?> builder()
{
return new Builder<Parent>()
{
@Override
public Parent build()
{
return new Parent(this);
}
};
}
protected Parent( Builder<?> builder )
{
this.parentProperty = builder.parentProperty;
}
}
class Child1 extends Parent
{
private final int child1Property;
public int getChild1Property()
{
return child1Property;
}
public static abstract class Builder<T extends Child1> extends Parent.Builder<T>
{
private int child1Property;
public Builder<T> withChild1Property( int child1Property )
{
this.child1Property = child1Property;
return this;
}
public abstract T build();
}
public static Builder<?> builder()
{
return new Builder<Child1>()
{
@Override
public Child1 build()
{
return new Child1(this);
}
};
}
protected Child1( Builder<?> builder )
{
super(builder);
this.child1Property = builder.child1Property;
}
}
class Child2 extends Parent
{
private final String child2PropertyA;
private final Object child2PropertyB;
public String getChild2PropertyA()
{
return child2PropertyA;
}
public Object getChild2PropertyB()
{
return child2PropertyB;
}
public static abstract class Builder<T extends Child2> extends Parent.Builder<T>
{
private String child2PropertyA;
private Object child2PropertyB;
public Builder<T> withChild2PropertyA( String child2PropertyA )
{
this.child2PropertyA = child2PropertyA;
return this;
}
public Builder<T> withChild2PropertyB( Object child2PropertyB )
{
this.child2PropertyB = child2PropertyB;
return this;
}
}
public static Builder<?> builder()
{
return new Builder<Child2>()
{
@Override
public Child2 build()
{
return new Child2(this);
}
};
}
protected Child2( Builder<?> builder )
{
super(builder);
this.child2PropertyA = builder.child2PropertyA;
this.child2PropertyB = builder.child2PropertyB;
}
}
class BuilderTest
{
public static void main( String[] args )
{
Child1 child1 = Child1.builder()
.withChild1Property(-3)
.withParentProperty(5L)
.build();
Child2 grandchild = Child2.builder()
.withChild2PropertyA("hello")
.withChild2PropertyB(new Object())
.withParentProperty(10L)
.build();
}
}
Здесь все еще есть шаблон: анонимный бетон Builder
в каждом методе builder()
и вызов super()
в каждом конструкторе. (Примечание: предполагается, что каждый уровень предназначен для дальнейшей наследуемости. Если в какой-то момент у вас есть потомок final
, вы можете сделать класс строителя конкретным и конструктором закрытым.)
Но я думаю, что эта версия проще следовать, для следующего программиста, который приходит и должен поддерживать ваш код (без самореферентных дженериков, для начинающих; Builder<X>
builds Xs
). И IMHO, требующее, чтобы дочерние свойства устанавливались в построителе до того, как родительские свойства имеют такое же преимущество, с точки зрения согласованности, поскольку это является недостатком с точки зрения гибкости.
Ответ 3
Может быть, это без строителей?:
interface P {
public Long getParentProperty();
}
interface C1 extends P {
public Integer getChild1Property();
}
interface C2 extends P {
public String getChild2PropertyA();
public Object getChild2PropertyB();
}
abstract class PABC implements P {
@Override public final Long getParentProperty() {
return parentLong;
}
protected Long parentLong;
protected PABC setParentProperty(Long value) {
parentLong = value;
return this;
}
}
final class C1Impl extends PABC implements C1 {
protected C1Impl setParentProperty(Long value) {
super.setParentProperty(value);
return this;
}
@Override public Integer getChild1Property() {
return n;
}
public C1Impl setChild1Property(Integer value) {
n = value;
return this;
}
private Integer n;
}
final class C2Impl extends PABC implements C2 {
private String string;
private Object object;
protected C2Impl setParentProperty(Long value) {
super.setParentProperty(value);
return this;
}
@Override public String getChild2PropertyA() {
return string;
}
@Override public Object getChild2PropertyB() {
return object;
}
C2Impl setChild2PropertyA(String string) {
this.string=string;
return this;
}
C2Impl setChild2PropertyB(Object o) {
this.object=o;
return this;
}
}
public class Myso9138027 {
public static void main(String[] args) {
C1Impl c1 = new C1Impl().setChild1Property(5).setParentProperty(10L);
C2Impl c2 = new C2Impl().setChild2PropertyA("Hello").setParentProperty(10L).setChild2PropertyB(new Object());
}
}
Ответ 4
Используйте generics следующим образом:
public interface Parent {
public Long getParentProperty();
}
public interface Child<T> {
public T getChildProperty();
}
Затем вместо Child1 используйте Child<Integer>
, а вместо Child2 используйте Child<String>
.
Ответ 5
package so9138027take2;
import java.util.*;
import so9138027take2.C2.Names;
interface P {
public Object getParentProperty(Names name);
enum Names {
i(Integer.class), d(Double.class), s(String.class);
Names(Class<?> clazz) {
this.clazz = clazz;
}
final Class<?> clazz;
}
}
interface C1 extends P {
public Object getChildProperty(Names name);
enum Names {
a(Integer.class), b(Double.class), c(String.class);
Names(Class<?> clazz) {
this.clazz = clazz;
}
final Class<?> clazz;
}
}
interface C2 extends P {
public Object getChildProperty(Names name);
enum Names {
x(Integer.class), y(Double.class), z(String.class);
Names(Class<?> clazz) {
this.clazz = clazz;
}
final Class<?> clazz;
}
}
abstract class PABCImmutable implements P {
public PABCImmutable(PABC parent) {
parentNameToValue = Collections.unmodifiableMap(parent.parentNameToValue);
}
@Override public final Object getParentProperty(Names name) {
return parentNameToValue.get(name);
}
public String toString() {
return parentNameToValue.toString();
}
final Map<Names, Object> parentNameToValue;
}
abstract class PABC implements P {
@Override public final Object getParentProperty(Names name) {
return parentNameToValue.get(name);
}
protected PABC setParentProperty(Names name, Object value) {
if (name.clazz.isInstance(value)) parentNameToValue.put(name, value);
else
throw new RuntimeException("value is not valid for " + name);
return this;
}
public String toString() {
return parentNameToValue.toString();
}
EnumMap<Names, Object> parentNameToValue = new EnumMap<Names, Object>(P.Names.class);
}
final class C1Immutable extends PABCImmutable implements C1 {
public C1Immutable(C1Impl c1) {
super(c1);
nameToValue = Collections.unmodifiableMap(c1.nameToValue);
}
@Override public Object getChildProperty(C1.Names name) {
return nameToValue.get(name);
}
public String toString() {
return super.toString() + nameToValue.toString();
}
final Map<C1.Names, Object> nameToValue;
}
final class C1Impl extends PABC implements C1 {
@Override public Object getChildProperty(C1.Names name) {
return nameToValue.get(name);
}
public Object setChildProperty(C1.Names name, Object value) {
if (name.clazz.isInstance(value)) nameToValue.put(name, value);
else
throw new RuntimeException("value is not valid for " + name);
return this;
}
public String toString() {
return super.toString() + nameToValue.toString();
}
EnumMap<C1.Names, Object> nameToValue = new EnumMap<C1.Names, Object>(C1.Names.class);
}
final class C2Immutable extends PABCImmutable implements C2 {
public C2Immutable(C2Impl c2) {
super(c2);
this.nameToValue = Collections.unmodifiableMap(c2.nameToValue);
}
@Override public Object getChildProperty(C2.Names name) {
return nameToValue.get(name);
}
public String toString() {
return super.toString() + nameToValue.toString();
}
final Map<C2.Names, Object> nameToValue;
}
final class C2Impl extends PABC implements C2 {
@Override public Object getChildProperty(C2.Names name) {
return nameToValue.get(name);
}
public Object setChildProperty(C2.Names name, Object value) {
if (name.clazz.isInstance(value)) {
nameToValue.put(name, value);
} else {
System.out.println("name=" + name + ", value=" + value);
throw new RuntimeException("value is not valid for " + name);
}
return this;
}
public String toString() {
return super.toString() + nameToValue.toString();
}
EnumMap<C2.Names, Object> nameToValue = new EnumMap<C2.Names, Object>(C2.Names.class);
}
public class So9138027take2 {
public static void main(String[] args) {
Object[] parentValues = new Object[] { 1, 2., "foo" };
C1Impl c1 = new C1Impl();
Object[] c1Values = new Object[] { 3, 4., "bar" };
for (P.Names name : P.Names.values())
c1.setParentProperty(name, parentValues[name.ordinal()]);
for (C1.Names name : C1.Names.values())
c1.setChildProperty(name, c1Values[name.ordinal()]);
C2Impl c2 = new C2Impl();
Object[] c2Values = new Object[] { 5, 6., "baz" };
for (P.Names name : P.Names.values())
c2.setParentProperty(name, parentValues[name.ordinal()]);
for (C2.Names name : C2.Names.values())
c2.setChildProperty(name, c2Values[name.ordinal()]);
C1 immutableC1 = new C1Immutable(c1);
System.out.println("child 1: "+immutableC1);
C2 immutableC2 = new C2Immutable(c2);
System.out.println("child 2: "+immutableC2);
}
}