Subclassificando uma Classe Java Builder


133

este artigo ao Dr. Dobbs e o Padrão do construtor em particular, como lidamos com o caso de subclassificar um construtor? Tomando uma versão resumida do exemplo em que queremos incluir uma subclasse para adicionar rotulagem de OGM, uma implementação ingênua seria:

public class NutritionFacts {                                                                                                    

    private final int calories;                                                                                                  

    public static class Builder {                                                                                                
        private int calories = 0;                                                                                                

        public Builder() {}                                                                                                      

        public Builder calories(int val) { calories = val; return this; }                                                                                                                        

        public NutritionFacts build() { return new NutritionFacts(this); }                                                       
    }                                                                                                                            

    protected NutritionFacts(Builder builder) {                                                                                  
        calories = builder.calories;                                                                                             
    }                                                                                                                            
}

Subclasse:

public class GMOFacts extends NutritionFacts {                                                                                   

    private final boolean hasGMO;                                                                                                

    public static class Builder extends NutritionFacts.Builder {                                                                 

        private boolean hasGMO = false;                                                                                          

        public Builder() {}                                                                                                      

        public Builder GMO(boolean val) { hasGMO = val; return this; }                                                           

        public GMOFacts build() { return new GMOFacts(this); }                                                                   
    }                                                                                                                            

    protected GMOFacts(Builder builder) {                                                                                        
        super(builder);                                                                                                          
        hasGMO = builder.hasGMO;                                                                                                 
    }                                                                                                                            
}

Agora, podemos escrever código assim:

GMOFacts.Builder b = new GMOFacts.Builder();
b.GMO(true).calories(100);

Mas, se errarmos o pedido, tudo falhará:

GMOFacts.Builder b = new GMOFacts.Builder();
b.calories(100).GMO(true);

É claro que o problema NutritionFacts.Builderretorna um NutritionFacts.Builder, não um GMOFacts.Builder, então como resolvemos esse problema ou existe um padrão melhor para usar?

Nota: esta resposta a uma pergunta semelhante oferece as classes que tenho acima; minha pergunta é sobre o problema de garantir que as chamadas do construtor estejam na ordem correta.


1
Eu acho que o link a seguir descreve uma boa abordagem: egalluzzo.blogspot.co.at/2010/06/…
stuXnet

1
Mas como você build()produz a saída b.GMO(true).calories(100)?
Sridhar Sarnobat 04/04

Respostas:


170

Você pode resolvê-lo usando genéricos. Eu acho que isso é chamado de "padrões genéricos curiosamente recorrentes"

Torne o tipo de retorno dos métodos do construtor da classe base um argumento genérico.

public class NutritionFacts {

    private final int calories;

    public static class Builder<T extends Builder<T>> {

        private int calories = 0;

        public Builder() {}

        public T calories(int val) {
            calories = val;
            return (T) this;
        }

        public NutritionFacts build() { return new NutritionFacts(this); }
    }

    protected NutritionFacts(Builder<?> builder) {
        calories = builder.calories;
    }
}

Agora instancie o construtor de base com o construtor de classe derivado como o argumento genérico.

public class GMOFacts extends NutritionFacts {

    private final boolean hasGMO;

    public static class Builder extends NutritionFacts.Builder<Builder> {

        private boolean hasGMO = false;

        public Builder() {}

        public Builder GMO(boolean val) {
            hasGMO = val;
            return this;
        }

        public GMOFacts build() { return new GMOFacts(this); }
    }

    protected GMOFacts(Builder builder) {
        super(builder);
        hasGMO = builder.hasGMO;
    }
}

2
Hmm, acho que vou ter que (a) postar uma nova pergunta, (b) redesenhar com em implementsvez de extendsou (c) jogar tudo fora. Agora tenho um erro de compilação estranho, onde leafBuilder.leaf().leaf()e leafBuilder.mid().leaf()é OK, mas leafBuilder.leaf().mid().leaf()não ...
Ken YN

11
@gkamal return (T) this;resulta em um unchecked or unsafe operationsaviso. Isso é impossível de evitar, certo?
Dmitry Minkovsky

5
Para resolver o unchecked castaviso, consulte a solução sugerida abaixo entre as outras respostas: stackoverflow.com/a/34741836/3114959
Stepan Vavra

8
Observe que, Builder<T extends Builder>na verdade, é um tipo bruto - deve ser Builder<T extends Builder<T>>.
Boris the Spider,

2
@ user2957378 o Builderfor GMOFactstambém precisa ser genérico Builder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>- e esse padrão pode continuar abaixo dos níveis necessários. Se você declarar um construtor não genérico, não poderá estender o padrão.
Boris the Spider

44

Apenas para o registro, para se livrar do

unchecked or unsafe operations Aviso

para a return (T) this;declaração como @dimadima e @Thomas N. falam, a solução a seguir se aplica em certos casos.

Faça abstracto construtor que declara o tipo genérico ( T extends Builderneste caso) e declareprotected abstract T getThis() o método abstrato da seguinte maneira:

public abstract static class Builder<T extends Builder<T>> {

    private int calories = 0;

    public Builder() {}

    /** The solution for the unchecked cast warning. */
    public abstract T getThis();

    public T calories(int val) {
        calories = val;

        // no cast needed
        return getThis();
    }

    public NutritionFacts build() { return new NutritionFacts(this); }
}

Consulte http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205 para obter mais detalhes.


Por que o build()método está retornando os NutrutionFacts aqui?
mvd 24/06

@vd Porque esta é uma resposta para a pergunta? Nos subtipos, você substituirá-o comopublic GMOFacts build() { return new GMOFacts(this); }
Stepan Vavra 24/06

O problema ocorre quando queremos adicionar 2ª criança BuilderC extends BuilderBe BuilderB extends BuilderAquando BuilderBnão éabstract
sosite

1
Esta não é uma resposta para a pergunta, porque a classe base pode não ser abstrata!
Roland

"Torne abstrato o construtor que declara o tipo genérico" - e se eu quisesse usar esse construtor diretamente?
daisy

21

Com base em uma postagem de blog , essa abordagem exige que todas as classes não-folha sejam abstratas e todas as classes de folha devem ser finais.

public abstract class TopLevel {
    protected int foo;
    protected TopLevel() {
    }
    protected static abstract class Builder
        <T extends TopLevel, B extends Builder<T, B>> {
        protected T object;
        protected B thisObject;
        protected abstract T createObject();
        protected abstract B thisObject();
        public Builder() {
            object = createObject();
            thisObject = thisObject();
        }
        public B foo(int foo) {
            object.foo = foo;
            return thisObject;
        }
        public T build() {
            return object;
        }
    }
}

Então, você tem alguma classe intermediária que estende essa classe e seu construtor, e quantas mais você precisar:

public abstract class SecondLevel extends TopLevel {
    protected int bar;
    protected static abstract class Builder
        <T extends SecondLevel, B extends Builder<T, B>> extends TopLevel.Builder<T, B> {
        public B bar(int bar) {
            object.bar = bar;
            return thisObject;
        }
    }
}

E, finalmente, uma classe de folhas de concreto que pode chamar todos os métodos do construtor em qualquer um de seus pais em qualquer ordem:

public final class LeafClass extends SecondLevel {
    private int baz;
    public static final class Builder extends SecondLevel.Builder<LeafClass,Builder> {
        protected LeafClass createObject() {
            return new LeafClass();
        }
        protected Builder thisObject() {
            return this;
        }
        public Builder baz(int baz) {
            object.baz = baz;
            return thisObject;
        }
    }
}

Em seguida, você pode chamar os métodos em qualquer ordem, a partir de qualquer uma das classes na hierarquia:

public class Demo {
    LeafClass leaf = new LeafClass.Builder().baz(2).foo(1).bar(3).build();
}

Você sabe por que as classes de folhas precisam ser finais? Eu gostaria que minhas classes concretas fossem subclassáveis, mas não encontrei uma maneira de fazer o compilador entender o tipo de coisa B, sempre é a classe base.
David Ganster

Observe como a classe Builder no LeafClass não segue o mesmo <T extends SomeClass, B extends SomeClass.Builder<T,B>> extends SomeClassParent.Builder<T,B>padrão que a classe intermediária SecondLevel, mas declara tipos específicos. Você não pode instalar uma classe até chegar à folha usando os tipos específicos, mas, uma vez que o fizer, não poderá mais estendê-la porque você está usando os tipos específicos e abandonou o Padrão de Modelo Curiosamente Recorrente. Este link pode ajudar: angelikalanger.com/GenericsFAQ/FAQSections/…
Q23

7

Você pode substituir também o calories()método e deixá-lo retornar o construtor de extensão. Isso é compilado porque o Java suporta tipos de retorno covariantes .

public class GMOFacts extends NutritionFacts {
    private final boolean hasGMO;
    public static class Builder extends NutritionFacts.Builder {
        private boolean hasGMO = false;
        public Builder() {
        }
        public Builder GMO(boolean val)
        { hasGMO = val; return this; }
        public Builder calories(int val)
        { super.calories(val); return this; }
        public GMOFacts build() {
            return new GMOFacts(this);
        }
    }
    [...]
}

Ah, eu não sabia disso, pois venho de um background em C ++. Essa é uma abordagem útil para este pequeno exemplo, mas com uma classe completa repetindo todos os métodos, torna-se uma dor e, portanto, propensa a erros. 1 por me ensinar algo novo, no entanto!
Ken YN

Parece-me que isso não resolve nada. O motivo (IMO) para subclassificar o pai é reutilizar os métodos dos pais sem substituí-los. Se as classes forem simplesmente objetos de valor sem lógica real nos métodos do construtor, exceto para configurar um valor simples, a chamada do método pai no método de substituição terá pouco ou nenhum valor.
Desenvolvedor Dude

A resposta resolve o problema descrito na pergunta: o código usando o construtor é compilado com os dois pedidos. Como uma maneira compila e a outra não, acho que deve haver algum valor, afinal.
Flavio

3

Há também outra maneira de criar classes de acordo com o Builderpadrão, que está de acordo com "Preferir composição sobre herança".

Defina uma interface que a classe pai Builderherdará:

public interface FactsBuilder<T> {

    public T calories(int val);
}

A implementação de NutritionFactsé quase a mesma (exceto para Builderimplementar a interface 'FactsBuilder'):

public class NutritionFacts {

    private final int calories;

    public static class Builder implements FactsBuilder<Builder> {
        private int calories = 0;

        public Builder() {
        }

        @Override
        public Builder calories(int val) {
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    protected NutritionFacts(Builder builder) {
        calories = builder.calories;
    }
}

A Builderclasse de uma criança deve estender a mesma interface (exceto implementação genérica diferente):

public static class Builder implements FactsBuilder<Builder> {
    NutritionFacts.Builder baseBuilder;

    private boolean hasGMO = false;

    public Builder() {
        baseBuilder = new NutritionFacts.Builder();
    }

    public Builder GMO(boolean val) {
        hasGMO = val;
        return this;
    }

    public GMOFacts build() {
        return new GMOFacts(this);
    }

    @Override
    public Builder calories(int val) {
        baseBuilder.calories(val);
        return this;
    }
}

Observe que esse NutritionFacts.Builderé um campo dentro GMOFacts.Builder(chamado baseBuilder). O método implementado a partir do método de FactsBuilderchamadas de interface baseBuildercom o mesmo nome:

@Override
public Builder calories(int val) {
    baseBuilder.calories(val);
    return this;
}

Há também uma grande mudança no construtor de GMOFacts(Builder builder). A primeira chamada no construtor para o construtor da classe pai deve passar apropriada NutritionFacts.Builder:

protected GMOFacts(Builder builder) {
    super(builder.baseBuilder);
    hasGMO = builder.hasGMO;
}

A implementação completa da GMOFactsclasse:

public class GMOFacts extends NutritionFacts {

    private final boolean hasGMO;

    public static class Builder implements FactsBuilder<Builder> {
        NutritionFacts.Builder baseBuilder;

        private boolean hasGMO = false;

        public Builder() {
        }

        public Builder GMO(boolean val) {
            hasGMO = val;
            return this;
        }

        public GMOFacts build() {
            return new GMOFacts(this);
        }

        @Override
        public Builder calories(int val) {
            baseBuilder.calories(val);
            return this;
        }
    }

    protected GMOFacts(Builder builder) {
        super(builder.baseBuilder);
        hasGMO = builder.hasGMO;
    }
}

3

Um exemplo completo de três níveis de herança de vários construtores seria assim :

(Para a versão com um construtor de cópias para o construtor, veja o segundo exemplo abaixo)

Primeiro nível - pai (potencialmente abstrato)

import lombok.ToString;

@ToString
@SuppressWarnings("unchecked")
public abstract class Class1 {
    protected int f1;

    public static class Builder<C extends Class1, B extends Builder<C, B>> {
        C obj;

        protected Builder(C constructedObj) {
            this.obj = constructedObj;
        }

        B f1(int f1) {
            obj.f1 = f1;
            return (B)this;
        }

        C build() {
            return obj;
        }
    }
}

Segundo nível

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class2 extends Class1 {
    protected int f2;

    public static class Builder<C extends Class2, B extends Builder<C, B>> extends Class1.Builder<C, B> {
        public Builder() {
            this((C) new Class2());
        }

        protected Builder(C obj) {
            super(obj);
        }

        B f2(int f2) {
            obj.f2 = f2;
            return (B)this;
        }
    }
}

Terceiro nivel

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class3 extends Class2 {
    protected int f3;

    public static class Builder<C extends Class3, B extends Builder<C, B>> extends Class2.Builder<C, B> {
        public Builder() {
            this((C) new Class3());
        }

        protected Builder(C obj) {
            super(obj);
        }

        B f3(int f3) {
            obj.f3 = f3;
            return (B)this;
        }
    }
}

E um exemplo de uso

public class Test {
    public static void main(String[] args) {
        Class2 b1 = new Class2.Builder<>().f1(1).f2(2).build();
        System.out.println(b1);
        Class2 b2 = new Class2.Builder<>().f2(2).f1(1).build();
        System.out.println(b2);

        Class3 c1 = new Class3.Builder<>().f1(1).f2(2).f3(3).build();
        System.out.println(c1);
        Class3 c2 = new Class3.Builder<>().f3(3).f1(1).f2(2).build();
        System.out.println(c2);
        Class3 c3 = new Class3.Builder<>().f3(3).f2(2).f1(1).build();
        System.out.println(c3);
        Class3 c4 = new Class3.Builder<>().f2(2).f3(3).f1(1).build();
        System.out.println(c4);
    }
}


Uma versão um pouco mais longa, com um construtor de cópias para o construtor:

Primeiro nível - pai (potencialmente abstrato)

import lombok.ToString;

@ToString
@SuppressWarnings("unchecked")
public abstract class Class1 {
    protected int f1;

    public static class Builder<C extends Class1, B extends Builder<C, B>> {
        C obj;

        protected void setObj(C obj) {
            this.obj = obj;
        }

        protected void copy(C obj) {
            this.f1(obj.f1);
        }

        B f1(int f1) {
            obj.f1 = f1;
            return (B)this;
        }

        C build() {
            return obj;
        }
    }
}

Segundo nível

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class2 extends Class1 {
    protected int f2;

    public static class Builder<C extends Class2, B extends Builder<C, B>> extends Class1.Builder<C, B> {
        public Builder() {
            setObj((C) new Class2());
        }

        public Builder(C obj) {
            this();
            copy(obj);
        }

        @Override
        protected void copy(C obj) {
            super.copy(obj);
            this.f2(obj.f2);
        }

        B f2(int f2) {
            obj.f2 = f2;
            return (B)this;
        }
    }
}

Terceiro nivel

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class3 extends Class2 {
    protected int f3;

    public static class Builder<C extends Class3, B extends Builder<C, B>> extends Class2.Builder<C, B> {
        public Builder() {
            setObj((C) new Class3());
        }

        public Builder(C obj) {
            this();
            copy(obj);
        }

        @Override
        protected void copy(C obj) {
            super.copy(obj);
            this.f3(obj.f3);
        }

        B f3(int f3) {
            obj.f3 = f3;
            return (B)this;
        }
    }
}

E um exemplo de uso

public class Test {
    public static void main(String[] args) {
        Class3 c4 = new Class3.Builder<>().f2(2).f3(3).f1(1).build();
        System.out.println(c4);

        // Class3 builder copy
        Class3 c42 = new Class3.Builder<>(c4).f2(12).build();
        System.out.println(c42);
        Class3 c43 = new Class3.Builder<>(c42).f2(22).f1(11).build();
        System.out.println(c43);
        Class3 c44 = new Class3.Builder<>(c43).f3(13).f1(21).build();
        System.out.println(c44);
    }
}

2

Se você não quer colocar os olhos em um colchete angular ou três, ou talvez não sinta você ... umm ... quero dizer ... tosse ... o resto de sua equipe rapidamente compreenderá com curiosidade padrão genérico recorrente, você pode fazer isso:

public class TestInheritanceBuilder {
  public static void main(String[] args) {
    SubType.Builder builder = new SubType.Builder();
    builder.withFoo("FOO").withBar("BAR").withBaz("BAZ");
    SubType st = builder.build();
    System.out.println(st.toString());
    builder.withFoo("BOOM!").withBar("not getting here").withBaz("or here");
  }
}

apoiado por

public class SubType extends ParentType {
  String baz;
  protected SubType() {}

  public static class Builder extends ParentType.Builder {
    private SubType object = new SubType();

    public Builder withBaz(String baz) {
      getObject().baz = baz;
      return this;
    }

    public Builder withBar(String bar) {
      super.withBar(bar);
      return this;
    }

    public Builder withFoo(String foo) {
      super.withFoo(foo);
      return this;
    }

    public SubType build() {
      // or clone or copy constructor if you want to stamp out multiple instances...
      SubType tmp = getObject();
      setObject(new SubType());
      return tmp;
    }

    protected SubType getObject() {
      return object;
    }

    private void setObject(SubType object) {
      this.object = object;
    }
  }

  public String toString() {
    return "SubType2{" +
        "baz='" + baz + '\'' +
        "} " + super.toString();
  }
}

e o tipo pai:

public class ParentType {
  String foo;
  String bar;

  protected ParentType() {}

  public static class Builder {
    private ParentType object = new ParentType();

    public ParentType object() {
      return getObject();
    }

    public Builder withFoo(String foo) {
      if (!"foo".equalsIgnoreCase(foo)) throw new IllegalArgumentException();
      getObject().foo = foo;
      return this;
    }

    public Builder withBar(String bar) {
      getObject().bar = bar;
      return this;
    }

    protected ParentType getObject() {
      return object;
    }

    private void setObject(ParentType object) {
      this.object = object;
    }

    public ParentType build() {
      // or clone or copy constructor if you want to stamp out multiple instances...
      ParentType tmp = getObject();
      setObject(new ParentType());
      return tmp;
    }
  }

  public String toString() {
    return "ParentType2{" +
        "foo='" + foo + '\'' +
        ", bar='" + bar + '\'' +
        '}';
  }
}

Pontos chave:

  • Encapsule o objeto no construtor para que a herança o impeça de definir o campo no objeto mantido no tipo pai
  • As chamadas para super garantem que a lógica (se houver) adicionada aos métodos do construtor supertipo seja retida nos subtipos.
  • O lado negativo é a criação de objetos espúrios na (s) classe (s) pai (s) ... Mas veja abaixo uma maneira de limpar isso
  • O lado positivo é muito mais fácil de entender rapidamente, e não há propriedades detalhadas de transferência de construtores.
  • Se você tiver vários encadeamentos acessando seus objetos construtores ... acho que estou feliz por não ser você :).

EDITAR:

Eu encontrei uma maneira de contornar a criação de objetos espúrios. Primeiro adicione isso a cada construtor:

private Class whoAmI() {
  return new Object(){}.getClass().getEnclosingMethod().getDeclaringClass();
}

Em seguida, no construtor para cada construtor:

  if (whoAmI() == this.getClass()) {
    this.obj = new ObjectToBuild();
  }

O custo é um arquivo de classe extra para a new Object(){}classe interna anônima


1

Uma coisa que você pode fazer é criar um método estático de fábrica em cada uma de suas classes:

NutritionFacts.newBuilder()
GMOFacts.newBuilder()

Esse método estático de fábrica retornaria o construtor apropriado. Você pode ter uma GMOFacts.Builderextensão NutritionFacts.Builder, isso não é um problema. O problema aqui será lidar com a visibilidade ...


0

A seguinte contribuição do IEEE, Refined Fluent Builder em Java, fornece uma solução abrangente para o problema.

Ele disseca a pergunta original em dois subproblemas de deficiência de herança e quase invariância e mostra como uma solução para esses dois subproblemas abre para o suporte à herança com reutilização de código no padrão clássico do construtor em Java.


Esta resposta não contém nenhuma informação para ser útil, não contém pelo menos um resumo da resposta fornecida no link e leva a um link que requer login.
Sonata

Esta resposta está vinculada a uma publicação da conferência revisada por pares com uma autoridade de publicação oficial e um procedimento oficial de publicação e compartilhamento.
mc00x1 2/06

0

Criei uma classe de construtor genérico abstrata pai que aceita dois parâmetros de tipo formal. Primeiro é para o tipo de objeto retornado por build (), o segundo é o tipo retornado por cada configurador de parâmetro opcional. Abaixo estão as classes pai e filho para fins ilustrativos:

// **Parent**
public abstract static class Builder<T, U extends Builder<T, U>> {
    // Required parameters
    private final String name;

    // Optional parameters
    private List<String> outputFields = null;


    public Builder(String pName) {
        name = pName;
    }

    public U outputFields(List<String> pOutFlds) {
        outputFields = new ArrayList<>(pOutFlds);
        return getThis();
    }


    /**
     * This helps avoid "unchecked warning", which would forces to cast to "T" in each of the optional
     * parameter setters..
     * @return
     */
    abstract U getThis();

    public abstract T build();



    /*
     * Getters
     */
    public String getName() {
        return name;
    }
}

 // **Child**
 public static class Builder extends AbstractRule.Builder<ContextAugmentingRule, ContextAugmentingRule.Builder> {
    // Required parameters
    private final Map<String, Object> nameValuePairsToAdd;

    // Optional parameters
    private String fooBar;


    Builder(String pName, Map<String, String> pNameValPairs) {
        super(pName);
        /**
         * Must do this, in case client code (I.e. JavaScript) is re-using
         * the passed in for multiple purposes. Doing {@link Collections#unmodifiableMap(Map)}
         * won't caught it, because the backing Map passed by client prior to wrapping in
         * unmodifiable Map can still be modified.
         */
        nameValuePairsToAdd = new HashMap<>(pNameValPairs);
    }

    public Builder fooBar(String pStr) {
        fooBar = pStr;
        return this;
    }


    @Override
    public ContextAugmentingRule build() {
        try {
            Rule r = new ContextAugmentingRule(this);
            storeInRuleByNameCache(r);
            return (ContextAugmentingRule) r;
        } catch (RuleException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    Builder getThis() {
        return this;
    }
}

Este atendeu minhas necessidades de satisfação.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.