Fluent builder - jak i czy wymuszać ustawianie pól?

0

Witam. Ostatnio interesowałem się tematem fluent buildera. Z tego co zrozumiałem przy wielu polach dla danego obiektu pojawia się problem. Jeśli będziemy chcieli je wszystkie ustawiać w konstruktorze, to konstruktor będzie nieczytelny. Z kolei jeśli będziemy chcieli ustawiać pola przez settery to możemy o jakichś zapomnieć i stworzyć obiekt z niezadeklarowanymi wartościami dla pól. Nie rozumiem za bardzo jednak jak fluent builder rozwiązuje ten drugi problem. Jeśli stworzę sobie klasę:

public class Person {

    private String name;
    private String surname;
    private int age;

    private Person(){}

    public String toString()
    {
        return "name: "+name+" surname: "+surname+" age: "+age;
    }

    public static final class Builder{

        private String name;
        private String surname;
        private int age;

        public Builder name (String name){
            this.name = name;
            return this;
        }

        public Builder surname (String surname){
            this.surname = surname;
            return this;
        }

        public Builder age (int age){
            this.age = age;
            return this;
        }

        public Person build(){
            Person person = new Person();
            person.name=name;
            person.surname=surname;
            person.age=age;
            return person;
        }
    }
}

Tworząc obiekt cały czas mogę ominąć ustawianie poszczególnych pól. Jeśli byłoby ich trzykrotnie więcej łatwo o tym po prostu zapomnieć, tak jak przy ustawianiu przez settery. Pobrałem parę projektów z GitHuba i w zasadzie w większości wygląda to tak samo. Raz widziałem żeby w tworzonym obiekcie były pola obowiązkowe i opcjonalne:

public final class Email {

    // To Address. Multiple Address separated by ","
    String to;
    //From Address
    String from;
    // Subject of the email
    String subject;
    // Content of the email
    String content;
    // BCC optional
    String bcc;
    // CC Optional
    String cc;

    /**
     * Private constructor to prevent the object initialization
     */
    private Email(String to, String from, String subject, String content, String bcc, String cc) {
        this.to = to;
        this.from = from;
        this.subject = subject;
        this.content = content;
        this.bcc = bcc;
        this.cc = cc;
    }

    public String getTo() {
        return to;
    }

    public String getFrom() {
        return from;
    }

    public String getSubject() {
        return subject;
    }

    public String getContent() {
        return content;
    }

    public String getBcc() {
        return bcc;
    }

    public String getCc() {
        return cc;
    }

    @Override
    public String toString() {
        return "Email{" +
                "to='" + to + '\'' +
                ", from='" + from + '\'' +
                ", subject='" + subject + '\'' +
                ", content='" + content + '\'' +
                ", bcc='" + bcc + '\'' +
                ", cc='" + cc + '\'' +
                '}';
    }

    // Interface to Set From
    interface EmailFrom {
        EmailTo setFrom(String from);
    }
    //Interface to Set To
    interface  EmailTo {
        EmailSubject setTo(String to);
    }
    //Interface to Set subject
    interface  EmailSubject {
        EmailContent setSubject(String subject);
    }
    // Interface to set Content
    interface  EmailContent {
        EmailCreator setContent(String content);
    }
    // Final Email Creator Class
    interface EmailCreator {

        EmailCreator setBCC(String bcc);
        EmailCreator setCC(String cc);
        Email build();
    }

    /** Static class for building the email object
     */
    public static class EmailBuilder implements  EmailFrom, EmailTo, EmailSubject, EmailContent, EmailCreator{

        String to;
        String from;
        String subject;
        String content;
        String bcc;
        String cc;

        /**
         * Private emailbuilder to prevent direct object creation
         */
        private EmailBuilder(){
        }

        /**
         * Getting the instance method
         * @return
         */
        public static EmailFrom getInstance(){
            return  new EmailBuilder();
        }

        @Override
        public EmailTo setFrom(String from) {
            this.from = from;
            return this;
        }

        @Override
        public EmailSubject setTo(String to) {
            this.to = to;
            return this;
        }

        @Override
        public EmailContent setSubject(String subject) {
            this.subject=subject;
            return this;
        }

        @Override
        public EmailCreator setContent(String content) {
            this.content=content;
            return this;
        }

        @Override
        public EmailBuilder setBCC(String bcc) {
            this.bcc=bcc;
            return this;
        }

        @Override
        public EmailBuilder setCC(String cc) {
            this.cc =cc;
            return this;
        }

        @Override
        public Email build() {
            return new Email(to,from,subject,content,bcc, cc);
        }
    }
}

Tylko nie bardzo wiem czy to jest warte zachodu. Po mojemu traci na tym wszystkim czytelność. Czy przypadkiem nie jest tak, że bardziej w tych fluent builderach chodzi o to, żeby z jednej strony konstruktor nie był za duży, a z drugiej żeby nie było setterów po to by osiągnąć niemutowalność?

0

żeby z jednej strony konstruktor nie był za duży

Nie chodzi tyle o rozmiar co o takie rzeczy jak np. problem z kolejnością parametrów. Wyobraź sobie konstruktor (int, int, int, int) który parametr jest który? Jaka szansa ze sie pomylisz? Builder pomaga tutaj bardzo. Dodatkowo co zrobisz jak połowa parametrów jest opcjonalna? Policz ile konstruktorów musisz napisać żeby obsłużyć 5 parametrów z których każdy można podać ale nie trzeba...

a z drugiej żeby nie było setterów po to by osiągnąć niemutowalność?

Tak, jedna z zalet jest taka że obiekt jest niemutowalny.

Nie rozumiem za bardzo jednak jak fluent builder rozwiązuje ten drugi problem.

Możesz sobie przeprowadzić weryfikacje pól kiedy wołasz build i sprawdzić czy są dobrze wypełnione. W przypadku settrów nie da się tak zrobić.

0

@paranoise: rozróżnijmy dwie kwestie:

  1. Wzorzec Builder.
  2. Fluent API.

Builder powstał po to, żeby przy np. przy N polach nie trzeba było tworzyć od cholery kostruktorów. O ile w obiektach mutowalnych to dosyć łatwo obejść, o tyle w obiektach niemutowalnych już trudniej.
Fluent API z kolei służy temu, aby człowiek tworzący coś nie pominął żadnego wymagalnego kroku.

I teraz - FluentBuilder to taki dziwny twór, który jednocześnie zapewnia niemutowalność, oraz to, że na etapie tworzenia obiektu programista nie zapomni o jakimś polu. Takie połączenie dwóch wzorców z definicji będzie wymagało dużo kodu i nie zawsze będzie warte zachodu. W podanym przez ciebie przykładzie taki FluentBuilder nawet nie ma zbytnio sensu, bo w każde pole można wrzucić cokolwiek - i wynikowy Email może nie mieć sensu.

FluentBuildera raczej nie używałbym do budowy POJO, tylko do konstrukcji jakichś bardziej skomplikowanych rzeczy.

1 użytkowników online, w tym zalogowanych: 0, gości: 1