Beispiel mit Zuständen

Nun folgt ein einfaches Beispiel in dem alles selbstgestrickt und über innere Klassen gelöst wurde. Ein wesentlicher Nachteil, ist die viele Schreibarbeit und die schlechte Lesbarkeit, da alles in einer Klasse steht. Theoretisch lässt sich die Schreibarbeit über Annotationsprozessoren lösen aber wenn man dies selbst tun möchte ohne direkte Bytecode Manipulationen zu nutzen, dann darf man die annotierte Klasse nicht ändern sondern nur die Generate. Daher gefällt mit die folgende Lösung nicht, auch wenn sie tut was sie soll.

StagedPerson.java
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

import java.time.LocalDate;


@Getter
public class StagedPerson {

    StagedPerson() {
    }

    protected double birthWeight;
    protected LocalDate birthday;
    protected String firstName;
    protected String sureName;


    // State birth
    protected int standesamtNummer;
    protected int registerNumber;
    protected int birthYear;


    // State citizen
    protected String address;
    protected String taxID;


    //
    // Begin of Stages
    //

    public interface New {
        Born birthWeight(final double weight);
    }

    public interface Born {
        Register birthday(final LocalDate birthday);
    }

    public interface Register {
        Register firstName(final String firstName);

        Register sureName(final String sureName);

        Registered register(final int registerNummer);
    }

    public interface Registered {
        StagedPerson build();
    }

    interface Stages extends New, Born, Register, Registered {

    }

    //
    // Begin of Builder
    //

    public static class StagedPersonBuilder implements Stages {

        final StagedPerson person;

        public StagedPersonBuilder() {
            person = new StagedPerson();
        }


        @Override
        public Born birthWeight(double birthWeight) {
            if (birthWeight < 0.1) throw new IllegalArgumentException("birthWeight must be greather then 100g !");
            person.birthWeight = birthWeight;
            return this;
        }

        @Override
        public Register birthday(LocalDate birthday) {
            if (birthday == null) throw new IllegalArgumentException("birthday must be not null");
            person.birthday = birthday;
            return this;
        }

        @Override
        public Register firstName(String firstName) {
            if (StringUtils.isEmpty(firstName)) throw new IllegalArgumentException("firstName must be not null");
            person.firstName = firstName;
            return this;
        }

        @Override
        public Register sureName(String sureName) {
            if (StringUtils.isEmpty(sureName)) throw new IllegalArgumentException("sureName must be not null");
            person.sureName = sureName;
            return this;
        }

        @Override
        public Registered register(int registerNumber) {
            if (registerNumber < 1) throw new IllegalArgumentException("registerNumber must be greather then 0!");
            person.registerNumber = registerNumber;
            return this;
        }

        @Override
        public StagedPerson build() {
            return person;
        }

    }

}

Viel besser gelungen ist mir die folgende Lösung. Hier wurde sämtliche Builderlogik in separate Klassen ausgelagert und dadurch sollte sich hier der Einsatz von Annotation Prozessoren deutlich mehr lohnen. Eine Unschönheit besitzt aber auch diese Lösung. Um die im Aufbau befindliche Instanz von einer Stage zur nächsten zu übertragen, ist der Aufruf einer Methode notwendig. Der Name dieser Methode würde sich normalerweise nach dem Klassennamen des aufzubauenden Objektes richten z.B. getPerson. Der Name würde dann auch der korrekten Semantik entsprechen. Da aber über diese Methode das aufzubauende Objekt zugänglich gemacht wird, würde eine getPerson() Methode aber einen Getter darstellen und somit das Builder Pattern verletzen. Beim Builder Pattern gibt es nur eine Methode, welche das aufzubauende Objekt zurückgeben darf und das ist die build() Methode. Daher wurde die Methode auch so benannt.

Leider muss diese build() Methode nun intern beim Stage Wechsel benutzt werden um das bisher erstellte Objekt weiter zu geben. Das ist die Unschönheit an dieser Lösung.

LegacyPerson.java
import lombok.Getter;

import java.time.LocalDate;

@Getter
public class LegacyPerson {

    LegacyPerson() {
    }

    protected double birthWeight;
    protected LocalDate birthday;
    protected String firstName;
    protected String sureName;


    // State birth
    protected int standesamtNummer;
    protected int registerNumber;
    protected int birthYear;


    // State citizen
    protected String address;
    protected String taxID;
}
LegacyPersonBuilder.java
public abstract class LegacyPersonBuilder implements BuilderStages {

    LegacyPerson person;

    private LegacyPersonBuilder() {
        this.person = new LegacyPerson();
    }


    public static NewStage builder() {
        final LegacyPersonBuilder builder = new LegacyPersonBuilder() {
            // will be never called, because is internal overridden by lambda in return of builder() method
            public LegacyPerson build() {
                throw new UnsupportedOperationException("Call of method forbidden");
            }
        };

        // old style replaced by lambda
        //    return new NewStage() {
        //        @Override
        //        public LegacyPerson getPerson() {
        //            return builder.person;
        //        }
        //    };
        return () -> builder.person;
    }

}
BuilderStages.java
import java.time.LocalDate;

// NewStage -> BornStage -> RegisterStage -> RegisteredStage
public interface BuilderStages {
    LegacyPerson build();

    interface NewStage extends BuilderStages {

        default BornStage birthWeight(double birthWeight) {
            final LegacyPerson person = build();
            if (birthWeight < 0.1) throw new IllegalArgumentException("birthWeight must be greather then 100g !");
            person.birthWeight = birthWeight;
            // unpleasant but necessary
            return this::build;
        }

    }

    interface BornStage extends BuilderStages {

        default RegisterStage birthday(LocalDate birthday) {
            final LegacyPerson person = build();
            if (birthday == null) throw new IllegalArgumentException("birthday must be not null");
            person.birthday = birthday;
            // unpleasant but necessary
            return this::build;
        }

    }

    interface RegisterStage extends BuilderStages {

        default RegisterStage firstName(String firstName) {
            final LegacyPerson person = build();
            if (firstName == null) throw new IllegalArgumentException("firstName must be not null");
            person.firstName = firstName;
            // possible because no stage change
            return this;
        }

        default RegisterStage sureName(String sureName) {
            final LegacyPerson person = build();
            if (sureName == null) throw new IllegalArgumentException("sureName must be not null");
            person.sureName = sureName;
            // possible because no stage change
            return this;
        }

        default RegisteredStage register(int registerNumber) {
            final LegacyPerson person = build();
            if (registerNumber < 1) throw new IllegalArgumentException("registerNumber must be greather then 0!");
            person.registerNumber = registerNumber;
            // unpleasant but necessary
            return this::build;
        }
    }

    interface RegisteredStage extends BuilderStages {
        // explizit build() definition not necessary because already defined in BuilderStages
    }

}