“Code is like humor. When you have to explain it, it’s bad.”
– Cory House
Immer wieder trifft man auf Klassen die diverse Parametern für ihre Konstruktoren gebrauchen. Abgesehen davon, dass zu viele Parameter ein Code Smell sind, kann es je nach Typ der Parameter auch schon einmal zu einer Verwechslung kommen.
public Profile(String firstName, String lastName, String title, LocalDate birth, Gender gender) { }
Der obige Konstruktor besitzt drei String Parameter und ein unvorsichtiger Entwickler hat diverse Möglichkeiten ihn fehlerhaft aufzurufen.
new Profile("Mr", "Chico", "Marx", LocalDate.of(1887, Month.MARCH, 22), Gender.MALE); new Profile("Harpo", "Mr", "Marx", LocalDate.of(1888, Month.NOVEMBER, 23), Gender.MALE); new Profile("Groucho", "Marx", "Mr", LocalDate.of(1890, Month.OCTOBER, 2), Gender.MALE); new Profile("Marx", "Gummo", "Mr", LocalDate.of(1892, Month.OCTOBER, 23), Gender.MALE); new Profile("Marx", "Mr", "Zeppo", LocalDate.of(1901 Month.FEBRUARY, 25), Gender.MALE);
Von den fünf dargestellten Konstruktoren, ist nur der für Groucho Marx korrekt. Eine andere Art der Konstruktion ist die Verwendung eines fluent Interfaces. Sehr vereinfacht handelt es sich bei einem fluent Interface um eine sprechende Schnittstellen mit einer Grammatik. In unserem Beispiel etwa
Profile.firstName("Chico").lastName("Marx").title("Mr").birth(LocalDate.of(1887, Month.MARCH, 22)).male();
In diesem Fall wäre firstname() eine statische Methode die ein Objekt mit dem Vornamen erzeugt und die weiteren Methoden würden die anderen Werte hinzufügen. Jede dieser Methoden würde das aktuelle Objekt vom Typ Profile zurückliefern und somit würde nach und nach das Objekt vollständig befüllt.
public static Profile firstName(String firstName) { return new Profile(firstName); } public Profile lastName(lastName) { this.lastName = Objects.requireNonNull(lastName); return this; }
Für einfache Anwendungsfälle ausreichend, wenn jedoch alle Attribute gesetzt sein müssen, oder eine spezielle Untermenge aller Attribute, dann ist dieser Ansatz sehr fehleranfällig. Strikter in der Verwendung und weniger fehleranfällig wird der Ansatz, wenn die Methoden intermediäre Objekte zurückliefern, die dem Entwickler die weiteren Schritte durch eine Grammatik vorgeben.
Person.female().firstName("Winifred").lastName("Banks").get(); Person.male().firstName("George").lastName("Banks").get();
Hier liefern die Methoden female() und male() intermediäre Objekte vom Typ FemaleBuilder, bzw. MaleBuilder, die dafür sorgen, dass am Ende entweder Frauen mit der Anrede “Mrs“, bzw. Männer mit der Anrede “Mr” erzeugt werden. Die anderen Methoden arbeiten auf diesen intermediären Objekten und erst der Aufruf von get() erzeugt dann ein Person Objekt.
class FemaleBuilder { private Profile profile = new Profile(Gender.FEMALE); FemaleBuilder firstName(String name) { profile.name = Objects.requireNonNull(name); return this; } FemaleBuilder title(String title) { profile.title= Objects.requireNonNull(title); return this; } return Profile get() { return profile; } } public static FemaleBuilder
female() {
return new FemaleBuilder(); }
Da es natürlich auch unverheiratete Frauen gibt, besitzt der FemalBuilder noch eine explizite title() Methode. Da in unserem Beispiel Männer keine andere Anrede haben können, gibt es diese Methode nicht in MaleBuilder.
Person.female().title("Ms").firstName("Mary").lastName("Poppins").get();
Insgesamt benötigt eine fluent Implementierung mehr Aufwand als ein einfacher Konstruktor, aber das bessere Verständnis und der weitaus geringere Dokumentationsaufwand ist diesen Aufwand wert.