WrRaThY
WrRaThY

Reputation: 1998

how to implement fluent builder with inheritance in java

problem

I want to create a class with a fluent builder, both of which can be inherited and extended. Base class should have all the common and mandatory fields, children should have different optional fields

simple example below (best, simplistic usecase I could come up with ;p)

base: Animal
    name
    age

    static Builder


impl: Snake extends Animal
    length

    static Builder extends Animal.Builder


impl: Spider extends Animal
    numberOfLegs

    static Builder extends Animal.Builder

and I'd like to use it in one of those ways (most preferred one is the first one):

Spider elvis = Spider.name("elvis").age(1).numberOfLegs(8).build();
Spider elvis = Spider.builder().name("elvis").age(1).numberOfLegs(8).build();
Spider elvis = new Spider.Builder().name("elvis").age(1).numberOfLegs(8).build();

what I want to achieve is

so far I failed and I'd be very grateful if you could please help me find a way out of it :) or maybe there is just a different approach that I should think about?

most valuable sources I used

http://blog.crisp.se/2013/10/09/perlundholm/another-builder-pattern-for-java
http://egalluzzo.blogspot.com/2010/06/using-inheritance-with-fluent.html
Generic fluent Builder in Java

work done so far

the code so far can be found below. there are some traces of the things I tried and failed, there are some unused or just weird stuff (best example is IBuildImpl). Those are left to give you an understanding of what I tried, but if you think that this needs moderation - please let me know and I'll clean them up

Base

package fafafa;

public abstract class Animal<T> {
    String name; //mandatory field, one of many
    Integer age; //mandatory field, one of many

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }


    interface IName {
        IAge name(String name);
    }

    interface IAge {
        IBuild age(Integer age);
    }

    interface IBuild<T extends Animal<T>> {
        T build();
    }


    public abstract static class Builder<T extends Animal<T>, B extends Builder<T, B>>
            implements IName, IAge, IBuild<T> {
        protected T objectBeingBuilt;

        protected abstract B that();
        protected abstract T createEmptyObject();

        Builder(){
            this.objectBeingBuilt = createEmptyObject();
            System.out.println();
        }

        @Override
        public IAge name(String name) {
            objectBeingBuilt.name = name;
            return that();
        }

        @Override
        public IBuild age(Integer age) {
            objectBeingBuilt.age = age;
            return that();
        }

//        @Override
//        public T build() {
//            return objectBeingBuilt;
//        }
    }


}

Impl

package fafafa;

public class Spider extends Animal<Spider> {
    Integer numberOfLegs; //optional field, one of many

    private Spider() {
    }

    public Integer getNumberOfLegs() {
        return numberOfLegs;
    }

    @Override
    public String toString() {
        return "Spider{" +
                "numberOfLegs='" + numberOfLegs + '\'' +
                "} " + super.toString();
    }

//    public static Builder<Spider, Builder> name(String name) {
//        return (Builder) new Builder().name(name);
//    }


    interface INumberOfLegs {
        IBuild numberOfLegs(Integer numberOfLegs);
    }

    interface IBuildImpl extends IBuild<Spider>, INumberOfLegs {
        @Override
        Spider build();
    }


    public static class Builder extends Animal.Builder<Spider, Builder> implements IBuildImpl {

        @Override
        protected Builder that() {
            return this;
        }

        @Override
        protected Spider createEmptyObject() {
            return new Spider();
        }


        public IBuild numberOfLegs(Integer numberOfLegs) {
            objectBeingBuilt.numberOfLegs = numberOfLegs;
            return that();
        }

        public Spider build() {
            return objectBeingBuilt;
        }
    }
}

Main

package fafafa;

public class Main {
    public static void main(String[] args) {
        Spider build = new Spider.Builder().name("elvis")
                .age(1)
                .numberOfLegs(8) //cannot resolve method numberOfLegs
                .build();
        System.out.println(build);
    }
}

Upvotes: 1

Views: 3151

Answers (3)

Ondřej Fischer
Ondřej Fischer

Reputation: 411

The problem of your code is the interface:

interface IAge {
    IBuild age(Integer age);
}

This will always return the basic IBuild interface with no parameter, no matter, if the implementation implements it with some argument. Actually even returning it with the parameter wouldn't extend the builder with additional methods.

  1. The parameter in the builder needs to be the extended builder, and not the type to be built.
  2. All interfaces for the common parameters need to be parametrized with it to allow propper continuation.

Here is a suggestion: 1. Don't use IName interface. Replace it with static entry method of the builder 2. Parametrize IAge interface 3. No common builder needed. It can be replaced with inline lambda implementation

Here is the code:

@FunctionalInterface
public interface IAge<B> {
    B age(Integer age);
}

public class AnimalBuilder implements IBuild<Animal> {

    private final String name;
    private final Integer age;
    private Integer numberOfLegs;

    private AnimalBuilder(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    // Builder entry method
    public static IAge<AnimalBuilder> name(String name) {
        return age -> new AnimalBuilder(name, age);
    }

    public AnimalBuilder numberOfLegs(int value) {
        numberOfLegs = value;
        return this;
    }

    @Override
    public Animal build() {
        return new Animal(name, age, numberOfLegs);
    }
}

This allows following usage:

AnimalBuilder.name("elvis").age(1).numberOfLegs(8).build();

Upvotes: 1

Maksym Prokhorenko
Maksym Prokhorenko

Reputation: 426

Looks like to many generics in a code, I've tried to simplify it a little.

Animal

package come.stackoverflow.builder;

public abstract class Animal {
  private final String name; //mandatory field, one of many
  private final Integer age; //mandatory field, one of many

  Animal(final String name, final Integer age) {this.name = name; this.age = age;}

  public String getName() {return name;}
  public Integer getAge() {return age;}
  @Override public String toString() {return String.format("Animal {name='%s', age='%s'}'", name, age);}

  interface IBuild<T> {
    T build();
  }

  public abstract static class AnimalBuilder<B extends AnimalBuilder, T extends Animal> implements IBuild<T> {
    String name;
    Integer age;

    public B name(final String name) {this.name = name; return (B) this;}
    public B age(final Integer age) {this.age = age; return (B) this;}
  }
}

Spider

package come.stackoverflow.builder;

public class Spider extends Animal {
  private final Integer numberOfLegs; //optional field, one of many

  private Spider(final String name, final Integer age, final Integer numberOfLegs) {super(name, age); this.numberOfLegs = numberOfLegs;}

  public Integer getNumberOfLegs() {return numberOfLegs;}
  @Override public String toString() {return String.format("Spider {numberOfLegs='%s'}, %s", getNumberOfLegs(), super.toString());}

  public static class SpiderBuilder extends AnimalBuilder<SpiderBuilder, Spider> {
    Integer numberOfLegs;
    public SpiderBuilder numberOfLegs(final Integer numberOfLegs) {this.numberOfLegs = numberOfLegs; return this;}
    public Spider build() {return new Spider(name, age, numberOfLegs);}
  }
}

Main Test

import come.stackoverflow.builder.Spider;

public class Main {
    public static void main(String[] args) {
        Spider build = new Spider.SpiderBuilder()
            .name("elvis").numberOfLegs(8).age(1)
            .build();
        System.out.println(build);
    }
}

Execution Result: Spider {numberOfLegs='8'}, Animal {name='elvis', age='1'}'

Upvotes: 3

davidxxx
davidxxx

Reputation: 131456

The problem is in the abstract builder :

public abstract static class Builder<T extends Animal<T>, B extends Builder<T, B>>
        implements IName, IAge, IBuild<T> {
 ...
    @Override
    public IAge name(String name) {
        objectBeingBuilt.name = name;
        return that();
    }

    @Override
    public IBuild age(Integer age) {
        objectBeingBuilt.age = age;
        return that();
    }

So, all your concrete builders return the same IBuild<T> interface when you invoke the age() method.

and as you see :

interface IBuild<T extends Animal<T>> {
    T build();
}

this interface doesn't allow to return a object where you have methods to set properties with your builder.

When you invoke the name() method, you also don't get the builder :

interface IAge {
    IBuild age(Integer age);
}

You should declare age() and name() in the abstract builder like that :

public abstract static class Builder<T extends Animal<T>, B extends Builder<T, B>>{
   ...

    public B name(String name) {
        objectBeingBuilt.name = name;
        return that();
    }


    public B age(Integer age) {
        objectBeingBuilt.age = age;
        return that();
    }

In this way, at the compile time, the concrete builder will return the builder of the animal you are creating when you will invokebuilder.age(..).
Besides, I don't understand why having a builder interface for name and another one for age.
What is interest to handle IAge and IName interfaces ?
It seems a too low level information to be useful in your builder.
Why not simply declaring you base builder like that :

public abstract static class Builder<T extends Animal<T>, B extends Builder<T, B>>
            implements IBuild<T> {
        protected T objectBeingBuilt;

        protected abstract B that();
        protected abstract T createEmptyObject();

        Builder(){
            this.objectBeingBuilt = createEmptyObject();
            System.out.println();
        }

        public B name(String name) {
            objectBeingBuilt.name = name;
            return that();
        }

        public B age(Integer age) {
            objectBeingBuilt.age = age;
            return that();
        }

    }

I have not tested the code.

Upvotes: 1

Related Questions