Fluent builder

Poslední dobou jsem několikrát řešil situaci, kdy se do kódu při inicializaci objektů dostaly příkazy, které znesnadňovaly čtení kódu a dávaly prostor pro zanesení chyb. Proto jsem hledal možnosti, jak těmto situacím zabránit. Jednou z možností je design pattern Fluent builder.

V článku bude tento návrhový vzor rozebrán. Jak již bylo naznačeno, jeho cílem je pomáhat vytvářet objekty typově bezpečným a čitelným způsobem, který je zároveň odolnější vůči chybám z nepozornosti. Jedná se o jednu z variant návrhového vzoru Builder, která funguje na principu Fluent interface.

Obecně existuje mnoho způsobů, jak vytvářet objekty:

  • konstruktorem,
  • static factory method,
  • factory pattern,

Pro názornost uveďme třídu User, s níž budeme v příkladech pracovat:


/* package a importy vynechány */
public class User {

  private Long id;
  private String firstName;
  private String lastName;
  private boolean enabled;
  private String address;
  private String phoneNumber;

  /* gettery a settery vynechány */
}

Možná nejběžnější způsob, jakým vytváříme objekty a nastavujeme jeho vlastnosti, je:


User user = new User();
user.setId(1L);
user.setEmail("petr.maly@inventi.cz");
user.setFirstName("Petr");
user.setLastName("Malý");
user.setEnabled(true);
user.setAddress("Lovosická 265, Horní dolní");
user.setPhoneNumber("775486325");

Co je na tomto způsobu špatného?

V této podobě nic. Nezabrání nám však napsat něco takového:


User user = new User();
user.setId(1L);
LOGGER.info("New user was created");
user.setEmail("petr.maly@inventi.cz");
mailSender.notify("petr.maly@inventi.cz");
user.setFirstName("Petr");
user.setLastName("Malý");
user.setEnabled(true);
System.out.println("User is enabled by default");
user.setAddress("Lovosická 265, Horní dolní");
user.setPhoneNumber("775486325");

Tohle ve svém projektu opravdu mít nechcete…

Alternativou je použít právě fluent builder pattern, jehož pomocí můžeme vytvořit stejný objekt takto:


User user = UserBuilder
  .anUser()
  .withId(1L)
  .withEmail("petr.maly@inventi.cz")
  .withFirstName("Petr")
  .withLastName("Malý")
  .withEnabled(true)
  .withAddress("Lovosická 265, Horní dolní")
  .withPhoneNumber("775486325")
  .build();

Sem žádná nesouvisející volání dát nemůžeme, a proto je kód čistší a zůstane takový i v budoucnu.

Jak vypadá samotný UserBuilder?


public class UserBuilder {
  private Long id;
  private String email;
  private String firstName;
  private String lastName;
  private boolean enabled;
  private String address;
  private String phoneNumber;

  private UserBuilder() {
  }

  public static UserBuilder anUser() {
    return new UserBuilder();
  }

  public UserBuilder withId(Long id) {
    this.id = id;
    return this;
  }

  public UserBuilder withEmail(String email) {
    this.email = email;
    return this;
  }

  public UserBuilder withFirstName(String firstName) {
    this.firstName = firstName;
    return this;
  }

  public UserBuilder withLastName(String lastName) {
    this.lastName = lastName;
    return this;
  }

  public UserBuilder withEnabled(boolean enabled) {
    this.enabled = enabled;
    return this;
  }

  public UserBuilder withAddress(String address) {
    this.address = address;
    return this;
  }

  public UserBuilder withPhoneNumber(String phoneNumber) {
    this.phoneNumber = phoneNumber;
    return this;
  }

  public UserBuilder but() {
    return anUser().withId(id).withEmail(email).withFirstName(firstName).withLastName(lastName).withEnabled(enabled).withAddress(address).withPhoneNumber(phoneNumber);
  }

  public User build() {
    User user = new User();
    user.setId(id);
    user.setEmail(email);
    user.setFirstName(firstName);
    user.setLastName(lastName);
    user.setEnabled(enabled);
    user.setAddress(address);
    user.setPhoneNumber(phoneNumber);
    return user;
  }
}

Pochopitelná námitka je, že psát builder je minimálně dvojnásob pracné v porovnání se situací, kdy máme jen třídu User.

Builder ale nemusíme psát ručně!

Ve vývojovém prostředí IntelliJ IDEA je možné buildery vytvářet pomocí pluginu Builder Generator (https://plugins.jetbrains.com/plugin/6585?pr=). Po nainstalování stačí použít klávesovou zkratku Alt + Shift + B a v dialogovém okně nastavit mj. prefix metod (v tomto případě “with”), název třídy a také to, zda se bude jednat o statickou vnitřní třídu.

Pro vývojové prostředí Eclipse existuje např. tento plugin https://marketplace.eclipse.org/content/fluent-builder-generator, který by měl dle popisu poskytovat stejné možnosti. Jeho funkčnost jsem však neověřoval.

Další námitkou proti fluent builderu může být, že je možné vytvořit jeden nebo více konstruktorů, příp. statických factory metod s různými kombinacemi parametrů a tím dosáhneme v podstatě téhož.

Ve výsledku můžeme skončit s několika konstruktory podobnými následujícímu:


public User(final Long id, final String email, final String firstName, final String lastName, final boolean enabled, final String address, final String phoneNumber) {
  this.id = id;
  this.email = email;
  this.firstName = firstName;
  this.lastName = lastName;
  this.enabled = enabled;
  this.address = address;
  this.phoneNumber = phoneNumber;
}

Zavoláme-li však takový konstruktor a chceme některá pole vynechat, musíme předávat null přesně tam, kam patří. Stejně tak hodnoty atributů musíme předávat na správné místo. V situaci, kdy se někdo jmenuje Milan Alois, je velmi snadné mu při volání konstruktoru prohodit jméno a příjmení. Aby se to nestalo, vše musíme několikrát zkontrolovat. Pokud pak kód čte někdo další, není schopen rozpoznat, kam který parametr směřuje bez toho, aniž by si konstruktor zobrazil.

Při použití fluent builderu tento problém odpadá, protože název metody with* jasně říká, který atribut nastavujeme, a pokud např. withPhoneNumber() vůbec nezavoláme, bude mít vlastnost phoneNumber hodnotu null.

Existuje dokonce možnost deklarovat pouze interface pro fluent builder a implementaci nechat vytvořit pomocí externí knihovny. Příkladem takové knihovny je https://github.com/davidmarquis/fluent-interface-proxy.

Shrnutí

Použití builderu má následující výhody:

  1. Pokud vytvářenému objektu nechceme některý atribut nastavovat, nemusíme volat příslušnou with*() metodu.
  2. Nemusíme ve třídě, jejíž instance pomocí builderu vytváříme, psát změť konstruktorů nebo factory metod s různým počtem a kombinací parametrů.
  3. Stejně jako u setterů vidíme, kterou vlastnost nastavujeme.
  4. Vlastnosti můžeme nastavovat v libovolném pořadí.
  5. Kód získá odolnost vůči přidávání nesouvisejících volání do kódu vytvářejícího objekt.

Nevýhody jsou:

  1. Je nutné vytvořit a udržovat samotné buildery (zmírníme např. použitím výše zmíněných pluginů).
  2. Je potřeba dodržet jednotnou podobu builderů v rámci projektu (aby různorodost nevedla spíše ke zmatení).
  3. Ostatním členům týmu je potřeba vysvětlit, jaké výhody jim to přinese, což vyžaduje určité úsilí.

 

 

Autor: Vít Herain

3 komentáře: „Fluent builder

  1. Ahoj, děkuji za článek. Buildery jsou dobrá věc. Jaký máš názor na a používáš Lombok? https://projectlombok.org/features/index.html je to anotační generátor setter/gettru buildru toString atd. Výhodou je, že se automaticky aktualizuje i po úpravě kódu. Takže nedojde k nesynchronostem.

    Dále se zmíněným chybným kódem jsem se hodně krát setkal, tento problém řeší bych spíš doporučoval řešit obecnými principy – Single Responsibility Principle.

    Používáš SOLID principles? Jaké je na ně tvůj názor a jak se ti osvědčili v praxy?

  2. Ahoj,

    Lombok používám, ale má bohužel svá úskalí. Lombok Buildery postrádají možnost definovat i poděděné atributy, takže je to často nepoužitelné.

    SOLID principy vídám víceméně všude. Např. Single Responsibility Principle je ale těžko uchopitelný (kdo dokáže spolehlivě určit, co je JEDNA zodpovědnost?). Liskov substitution principle se zase poměrně často porušuje…

Komentáře nejsou povoleny.