Сохранение строителя в отдельном классе (свободный интерфейс)

Foo foo = Foo.builder()
    .setColor(red)
    .setName("Fred")
    .setSize(42)
    .build();

Итак, я знаю, что для вызова именованных параметров при вызове метода существует следующее "Builder". Хотя, похоже, это работает только с внутренними статическими классами в качестве строителя или я ошибаюсь? Я взглянул на некоторые уроки для шаблона построения, но они кажутся очень сложными для того, что я пытаюсь сделать. Есть ли способ сохранить класс Foo и класс Builder отдельно, имея преимущество названных параметров, таких как код выше?

Ниже стандартной настройки:

public class Foo {
    public static class Builder {
        public Foo build() {
            return new Foo(this);
        }

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

        public Builder setColor(Color color) {
            this.color = color;
            return this;
        }

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

        // you can set defaults for these here
        private int size;
        private Color color;
        private String name;
    }

    public static Builder builder() {
        return new Builder();
    }

    private Foo(Builder builder) {
        size = builder.size;
        color = builder.color;
        name = builder.name;
    }

    private final int size;
    private final Color color;
    private final String name;
}

Ответ 1

Используйте композицию. Чтобы сделать вещи проще и чище, не реплицируйте все атрибуты в классе источника (Foo) и строителя (Builder).

Например, у меня есть Foo class внутри Builder вместо каждого из атрибутов Foo.

простой фрагмент кода:

import java.util.*;

class UserBasicInfo{
    String nickName;
    String birthDate;
    String gender;

    public UserBasicInfo(String name,String date,String gender){
        this.nickName = name;
        this.birthDate = date;
        this.gender = gender;        
    }

    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("Name:DOB:Gender:").append(nickName).append(":").append(birthDate).append(":").
        append(gender);
        return sb.toString();
    }
}

class ContactInfo{
    String eMail;
    String mobileHome;
    String mobileWork;

    public ContactInfo(String mail, String homeNo, String mobileOff){
        this.eMail = mail;
        this.mobileHome = homeNo;
        this.mobileWork = mobileOff;
    }    
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("email:mobile(H):mobile(W):").append(eMail).append(":").append(mobileHome).append(":").append(mobileWork);
        return sb.toString();
    }
}
class FaceBookUser {
    String userName;
    UserBasicInfo userInfo;
    ContactInfo contactInfo;

    public FaceBookUser(String uName){
        this.userName = uName;
    }    
    public void setUserBasicInfo(UserBasicInfo info){
        this.userInfo = info;
    }
    public void setContactInfo(ContactInfo info){
        this.contactInfo = info;
    }    
    public String getUserName(){
        return userName;
    }
    public UserBasicInfo getUserBasicInfo(){
        return userInfo;
    }
    public ContactInfo getContactInfo(){
        return contactInfo;
    }

    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("|User|").append(userName).append("|UserInfo|").append(userInfo).append("|ContactInfo|").append(contactInfo);
        return sb.toString();
    }

    static class FaceBookUserBuilder{
        FaceBookUser user;
        public FaceBookUserBuilder(String userName){
            this.user = new FaceBookUser(userName);
        }
        public FaceBookUserBuilder setUserBasicInfo(UserBasicInfo info){
            user.setUserBasicInfo(info);
            return this;
        }
        public FaceBookUserBuilder setContactInfo(ContactInfo info){
            user.setContactInfo(info);
            return this;
        }
        public FaceBookUser build(){
            return user;
        }
    }
}
public class BuilderPattern{
    public static void main(String args[]){
        FaceBookUser fbUser1 = new FaceBookUser.FaceBookUserBuilder("Ravindra").build(); // Mandatory parameters
        UserBasicInfo info = new UserBasicInfo("sunrise","25-May-1975","M");

        // Build User name + Optional Basic Info 
        FaceBookUser fbUser2 = new FaceBookUser.FaceBookUserBuilder("Ravindra").
                                                setUserBasicInfo(info).build();

        // Build User name + Optional Basic Info + Optional Contact Info
        ContactInfo cInfo = new ContactInfo("[email protected]","1111111111","2222222222");
        FaceBookUser fbUser3 = new FaceBookUser.FaceBookUserBuilder("Ravindra").
                                                setUserBasicInfo(info).
                                                setContactInfo(cInfo).build();

        System.out.println("Facebook user 1:"+fbUser1);
        System.out.println("Facebook user 2:"+fbUser2);
        System.out.println("Facebook user 3:"+fbUser3);
    }
}

выход:

Facebook user 1:|User|Ravindra|UserInfo|null|ContactInfo|null
Facebook user 2:|User|Ravindra|UserInfo|Name:DOB:Gender:sunrise:25-May-1975:M|ContactInfo|null
Facebook user 3:|User|Ravindra|UserInfo|Name:DOB:Gender:sunrise:25-May-1975:M|ContactInfo|email:mobile(H):mobile(W):[email protected]:1111111111:2222222222

Пояснение:

  • FaceBookUser - сложный объект с атрибутами ниже с использованием композиции:

    String userName;
    UserBasicInfo userInfo;
    ContactInfo contactInfo;
    
  • FaceBookUserBuilder - это класс статического строителя, который содержит и строит FaceBookUser.

  • userName - это только обязательный параметр для создания FaceBookUser

  • FaceBookUserBuilder строит FaceBookUser, устанавливая необязательные параметры: UserBasicInfo и ContactInfo

  • В этом примере показаны три разных FaceBookUsers с разными атрибутами, построенными из Builder.

    • fbUser1 был создан как FaceBookUser только с атрибутом userName
    • fbUser2 был создан как FaceBookUser с именем пользователя и UserBasicInfo
    • fbUser3 был создан как FaceBookUser с именем пользователя, UserBasicInfo и ContactInfo

В этом примере композиция использовалась вместо дублирования всех атрибутов FaceBookUser в классе Builder.

EDIT:

Группировать все связанные атрибуты в логические классы. Определите все эти классы в FaceBookUser. Вместо добавления всех этих переменных-членов в Builder, введите FaceBookUser в классе Builder.

Для простоты я добавил два класса: UserBasicInfo и ContactInfo. Теперь взорвите этот класс FaceBookUser с другими атрибутами, например

NewsFeed
Messages
Friends
Albums
Events
Games
Pages
Ads

и др.

Если вы дублируете все эти атрибуты как в Builder, так и в FaceBookUser, код будет трудно справиться. Вместо этого, используя композицию FaceBookUser в FaceBookUserBuilder, вы можете просто построить процесс.

После добавления выше атрибутов вы будете строить FaceBookUser в пошаговом процессе, как обычно.

Это будет так:

FaceBookUser fbUser3 = new FaceBookUser.FaceBookUserBuilder("Ravindra").
                                        setUserBasicInfo(info).
                                        setNewsFeed(newsFeed).
                                        setMessages(messages).
                                        setFriends(friends).
                                        setAlbums(albums).
                                        setEvents(events).
                                        setGames(games).
                                        setAds(ads).build();

Ответ 2

Вы можете уверенно изменить поля своего класса Builder как частные - тогда вам нужен только (общедоступный) метод getter для каждого "свойства" в построителе; и конструктор в Foo называет эти методы; вместо того, чтобы просто извлекать поля в объекте Builder.

Затем вы можете просто переместить класс Builder из Foo. Простой и понятный.

Но помните: в конце концов, Builder и Foo очень тесно связаны. Они имеют общий дизайн обычных полей. Поэтому любое изменение Foo влияет на Builder; и наоборот. Таким образом, имеет смысл держать их "близко друг к другу". Может быть, не как внутренний/внешний класс, а, возможно, все еще в одном исходном файле! Но тогда... только один из них может быть общедоступным. Это действительно то, что вы хотите?!

Другими словами: не разрывайте вещи просто "потому что вы можете". Только делайте это, если у вас есть веские причины для этого, и если что-то из этого будет лучше, чем ваше текущее решение!

Изменить: ваша проблема может быть не разделение Foo и Builder, а тот факт, что ваш класс Foo имеет слишком много полей в первую очередь. Не забывайте об одном принципе ответственности... когда вашему классу требуется более 5, 6 полей... это, вероятно, слишком много, и их нужно еще нарезать! Имейте в виду: хороший дизайн OO - это прежде всего поведение ; не имея 10, 20 полей внутри какого-либо объекта!

Ответ 3

В качестве альтернативы шаблону строителя вы также можете использовать объект :

class FooParams {
  public int size;
  public Color color;
  public String name;
}

Вы можете использовать геттеры и сеттеры здесь, а не публичные поля, если хотите.

Затем конструктор Foo принимает один из них в качестве аргумента:

public Foo(FooParams params) {
  this.size = params.size;
  this.color = params.color;
  this.name = params.name;
}

Ответ 4

Трудно строго определить " Builder Pattern ™", и есть несколько степеней свободы относительно выбора дизайна. Некоторые концепции могут легко смешиваться или злоупотреблять, и кроме того, обычно трудно (и почти всегда неправильно) говорить "вам всегда нужно делать это именно так".

Вопрос заключается в том, что должно быть достигнуто путем применения "шаблона". В вашем вопросе и примере вы уже смешали две концепции, а именно шаблон конструктора и свободный интерфейс. Играя адвоката дьявола, можно было даже скрытно утверждать, что "Строитель" в вашем случае - это просто объект параметров, о котором уже говорил Томас, который построен особым образом (плавно ) и обогащается некоторой сложной комбинацией видимости public и private.

Некоторые из возможных целей шаблона построителя перекрываются или идут рука об руку. Но вы должны спросить себя, какая главная цель в вашем случае:

  • Должен ли результирующий объект быть неизменным?
    • Должен ли он быть действительно неизменным, только с окончательными полями final, или же могут быть сеттеры, которые просто не должны быть общедоступными? (Строитель все равно может позвонить этим непубличным сеттерам!)
  • Является ли цель ограничить видимость в целом?
  • Должен ли быть полиморфный экземпляр?
  • Основная цель состоит в том, чтобы суммировать большое количество параметров конструктора?
  • Основная цель - упростить настройку с помощью свободного интерфейса и управлять значениями по умолчанию? ...

Поскольку все эти вопросы будут влиять на тонкие различия в дизайне. Однако в отношении вашего фактического, высокого уровня, "синтаксического" вопроса:

  • Вы можете создать строитель как внутренний класс public static (что вы сделали в примере).

    public class Person { 
        ...
        public static PersonBuilder create() { ... }
    
        public static class PersonBuilder { 
            ...
            public Person build() { ... }
        }
    }
    

    Это самая строгая форма конфиденциальности: конструкторы Person и PersonBuilder могут быть private.

  • Вы также можете поместить фактический класс и его построитель в отдельные файлы:

    public class Person { 
        ...
    }
    

    и

    public class PersonBuilder { 
        ...
    }
    

    Здесь может быть достигнута разумная степень конфиденциальности: конструкторы обоих могут быть частными (например, иметь видимость по умолчанию).

В обоих случаях фактическое использование для клиентов будет одинаковым, за исключением имени класса строителя (package.Person.PersonBuilder vs. package.PersonBuilder). "Содержание" классов также было бы одинаковым (за исключением немного разных возможностей). И в обоих случаях вы можете создавать подклассы Person, если это необходимо, в зависимости от конфигурации строителя, а сам строитель может иметь свободный интерфейс.