Pull to refresh

Comments 86

Внутренние классы используются часто.

Вместо
public Account build() {
    Account account = new Account();
    account.userId = Account.this.userId;
    account.token = Account.this.token;

    return account;
}

можно реализовать Cloneable. Это избавляет от вероятных ошибок при добавлении полей. И на 20 полях будет разница в объеме в пользу Cloneable.
Про Cloneable хорошее замечание.

Можете привести примеры, где используются нестатические неанонимные внутренние классы? По крайней мере в тех проектах, где я успел поучаствовать за почти 10 лет работы не то что частого, вообще хоть какого-то их использования я не видел.
Очень любопытный опыт. У меня в каждом первом классе есть внутренние.
Разве в clone вам не нужно будет проделать по факту тоже самое? Или вы хотите делегировать вызов в super.clone()?
Это не всегда сработает. К примеру если есть mutable поля. Придется все-равно их ручками прописывать. Так что clone не предотвращает от появления ошибок в ходи копирования объекта.
Вы имеете ввиду, что копия будет не «глубокая»? Ну так и в исходном варианте это надо учитывать, не забывать. А еще делать копии в сеттере билдера. А еще делать defensive копии при возвращении из геттера…
Именно. Согласен полностью.
При таком подходе теряется важное свойство неизменяемости. К тому же после build() по-прежнему можно менять поля через тот же Builder.
Также ваш Builder не может быть фабрикой, т.к. повторный build() вернёт уже существующий объект.
Я об этом написал, ближе к концу топика. При необходимости можно сделать фабрику, коментарием выше подсказывают, что даже без необходимости ручного копирования полей.
Достаточно бессмысленно делать билдеры для таких бинов. Билдер имеет смысл, если есть некоторая логика создания и инициализации, иначе можно просто из сеттеров возвращать объект чтобы делать цепочки вызовов. Ведь иммутабельности тут нет, а копировать объект всё время бывает накладно.
Ведь иммутабельности тут нет

Поля приватные, сеттеров нет. Единственный способ изменить поля объекта без рефлексии — только через билдер, а у него время жизни короткое.
Чем не иммутабельность?
сделай поля final, тогда они станут немутабельными
20 финальных полей?! Их все в конструкторе инициализировать?

Одно из назначений этого паттерна в том, чтобы избавиться от конструтора с кучей параметров и при этом сделать класс немутабельным.
вот именно! для этого они мутабельные в билдере, и немутабельные в основном классе
UFO just landed and posted this here
Тогда весь набор полей надо повторить в билдере. Имеет смысл в случае когда очень нужно сделать поля final.
Можете аргументированно пояснить почему класс Account из топика не немутабелен? Если вам пришла откуда-то ссылка на объект этого класса, через его интерфейс вы никак значения полей не измените. Если я правильно понимаю, с точки зрения многопоточности тоже все ок, другие треды не видят ссылку на частично собраный объект. final в самом первом примере Account больше для читабельности, если их убрать код будет работать в точности как и с ними. Я в чем-то заблуждаюсь?
посмотрите Effective Java Блоха (имхо должна быть настольной книгой каждого джава дева) там есть и определение неизменяемого класса и объяснения почему это важно

в частности, без final на полях Вы не получите от JVM тех же гарантий видимости для многопоточного кода
Не могу понять о чем Вы говорите. Можете меня ткнуть носом в нужный Item из книги?
без final на полях Вы не получите от JVM тех же гарантий видимости для многопоточного кода

Модификатор final используется только на этапе компиляции. В байткоде его нет и JVM про него ничего не знает. Какие «гарантии видимости» вы имеете ввиду?
Вот ответ на stackoverflow Коротко: компилятор может менять инструкции местами и для другого треда все будет выглядеть так, что объект сначала создали, а только потом выставили значения полей. final явно говорит компилятору, что так делать не стоит.
А, теперь ясно. Спасибо.

Эта проблема связана с тем, что в байткоде создание объекта это две инструкции — NEW и INVOKESPECIAL.

Тем не менее Билдера эта проблема не касается. Вызов конструктора происходит в отдельном методе build() и только потом возвращается ссылка на объект. Даже если компилятор поменяет порядок внутри метода, ни один конкурентный поток не увидит ссылку на недостоенный объект. Разве что компилятор решит сделать inline методу, хотя я уверен, что на публичный метод у него рука не поднимется.
Разве что компилятор решит сделать inline методу, хотя я уверен, что на публичный метод у него рука не поднимется.

А почему нет, JIT-компилятор может заинлайнить код вашего метода build(), и уже после этого переставить инициализацию полей в произвольном порядке, в результате чего другие потоки увидят ваш объект еще до того, как будут записаны какие-то поля.

ну т.е.

Account acc = Account.newBuildler().setToken(a).setUserId(b).build();
this.account = acc;
doSomeAction(acc);


JIT-компилятор может превратить в:

Account acc = new Account();
this.acc = acc;
<вот тут из другого потока происходит чтение token, и он получает null...>
acc.token = a;
acc.userId = b;
doSomeAction(acc);
Дело не только в JIT-компиляции, а ещё в реордеринге в CPU и в протоколах когерентности кэшей.
Ну при наличии правильно расставленных барьеров реордеринг не будет проблемой. Беда в том, что если поле не-final, то JIT-компилятор не будет ставить лишние барьеры.
UFO just landed and posted this here
Про модификатор сказал ниже — перепутал с локальными переменными.
После прочтения Java memory model в JLS я в этом уже не далеко не уверен.

К сожалению очевидно ошибочный комментарий удалить уже нельзя.
UFO just landed and posted this here
Исходя из каких убеждений вы сделали эти утверждения?


Я имел ввиду Java компилятор, не JIT.

Если бы Java компилятор мог самовольно инлайнить публичные методы, мы бы остались без библиотек.
С JIT ситуация другая. Он работает уже с окончательным кодом, который нужно выполнить и волен решать сам как это делать.

Тем не менее проблема билдера и многопоточности несколько надумана. Точнее она ничем не отличается от обычного вызова конструктора в многпоточном коде. Очевидно, это задача клиентского кода синхронизировать доступ к общим ресурсам. Как, например, при использовании StringBuilder'а.
UFO just landed and posted this here
действительно, речь же о всего лишь «элегантном» билдере а не потокобезопасном, чего все так накинулись )
Спутал с локальными переменными и параметрами
Ну какая же это иммутабельность, если
Account.Builder builder = Account.newBuilder();
        
Account account = builder
            .setToken("hello")
            .setUserId("habr")
            .build();

assert account.getToken().equals("hello"); // ok

builder.setToken("blablabla");
        
assert account.getToken().equals("hello"); // ой
Это ж уже осуждалось. Ctrl-F «Важные замечания».

Кроме того, программирование — это всегда поиск компромисов. Между производительностью, количеством кода и безопасностью использования. Если типичное использование билдера выглядит так:

Account account = Account.newBuilder().setToken("hello").setUserId("habr").build();


То можно пренебречь возможностью менять объект через билдер. Вконце концов даже значение final поля можно изменить через рефлекшн, если SecurityManager это позволяет.
Есть такое правило: если ошибку можно совершить, кто-то её обязательно совершит. Если вы этот код написали только для себя, то и никаких вопросов. Если им будет пользоваться вся команда, то я бы выбрал решение, где такую ошибку совершить нельзя. Скорее всего это был бы просто бин, которому в конструктор можно передать значения полей. Без флюэнтов и билдеров. Для таких бинов это очень эффективный способ и вполне удобный.
Все уже было придумано до нас (а так же доведено до ума и отшлифовано до блеска):

@Getter
@Builder
public class Account {
    
    private String userId;
    private String token;     
}


Для параноиков (типа меня) есть вариант без использования публичного конструктора и c финальными полями:

@Getter
public class Account {
    
    private final String userId;
    private final String token;
    
    @Builder
    private Account(String userId, String token) {
        this.userId = userId;
        this.token = token;
    }
 }   
  


Использование в обоих случаях одинаковое:
Account.builder().userId("u12").token("t12").build(); 


Подробности на projectlombok.org.
А если нет желания тащить весь lombok, можно использовать github.com/google/auto
билдера пока нет, но должен когда-нибудь появиться — было бы логичным развитием
Как раз там есть в issues вопрос, будет ли билдер, и авторы ответили, что, скорее всего, нет.
Спасибо за ссылку, с первого взгляда рвет шаблон в клочья. Буду изучать.
К сожалению плагин lombok для idea не поспевает за новыми версиями ide и глючит, по этой причине когда то отказался от использования его в проекте.
Передали мне как-то проект, использующий lombok. Сперва было очень неочевидно, как работает вся эта магия, плюс надо куда-то лазить в настройки, чтобы идейка научилась процессить эти аннотации.

Первым делом избавился от зависимости от lombok.
Возможно, вы поторопились. Не стоит становиться рабом лампы, даже если это замечательная Идея. lombok работает с эклипс, нетбинс и чудесно билдется в мавен. В конце концов, javaagent API появилась не вчера, а в jdk5. И потому уже давно не магия, а инструмент, который должен удобно настраиваться в любой современной IDE.
Оно жеж работает через annotation processing (легко увидеть здесь), а не через instrumentation.
Сейчас точно не помню, но кажется, с использованием lombok у меня были проблемы, что некуда поставить breakpoint в setter, т.к. этого самого setter-а в коде нет.

Также, я стараюсь руководствоваться принципом наименьшего удивления, т.к. любой новый человек, который придет на проект, очень озадачится, как работает код, когда нет геттеров/сеттеров, хотя они вызываются.
Для явы самым простым и рабочим решением стал бы плагин для ИДЕ, который генерирует код билдера для конкретного объекта (как toString и тп.).

Выглядит вполне ожидаемо, думаю обсуждение можно закрывать :)
Уж лучше Scala. Она хоть идеей нормально поддерживается.
А вообще, идея отлично умеет генерить подобные билдеры.
Только плагинами, из коробки в 14 этого нет.
Криво выразился. Я имел ввиду, что плагин Scala отлично работает, в отличие от плагина lombok.
Я тоже некорректно выразил мысль. Имел ввиду, что идея без плагинов не имеет генерации билдеров.
Действительно, стандартно только Fluent Interface генерит.
Вариант со static inner class можно реализовать и без копий полей оригинала. Достаточно хранить собираемый объект. Естественно, что это не работает для immutable классов.

example
public class Account {
  private String userId;
  private String token;

  private Account() {}

  // getters

  // builder fabric method
  public static Builder builder() {
    return new Builder();
  }

  public static class Builder {
    private Account account = new Account();
    
    public Account build() {
      Account result = account;
      account = new Account();
      return result;
    }

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

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


// usage

Account account = Account.builder().userId(someId).token(someToken).build();

Account.Builder builder = Account.builder();
Account accountOne = builder.userId("1").token("t1").build();
Account accountTwo = builder.userId("2").build(); // token will be null
Идите к нам, у нас печеньки. В том числе именованные параметры, из-за отсутствия которых нужен очередной паттерн.

C# 5.0 (текущая версия):

public class Person
{
    public string LastName { get; private set; }
    public string FirstName { get; private set; }
    public string MiddleName { get; private set; }
    public string Salutation { get; private set; }
    public string Suffix { get; private set; }
    public string StreetAddress { get; private set; }
    public string City { get; private set; }
    public string State { get; private set; }
    public bool IsFemale { get; private set; }
    public bool IsEmployed { get; private set; }
    public bool IsHomeOwner { get; private set; }

    public Person (string lastName = "",
        string firstName = "",
        string middleName = "",
        string salutation = "",
        string suffix = "",
        string streetAddress = "",
        string city = "",
        string state = "",
        bool isFemale = false,
        bool isEmployed = false,
        bool isHomeOwner = false)
    {
        LastName = lastName;
        FirstName = firstName;
        MiddleName = middleName;
        Salutation = salutation;
        Suffix = suffix;
        StreetAddress = streetAddress;
        City = city;
        State = state;
        IsFemale = isFemale;
        IsEmployed = isEmployed;
        IsHomeOwner = isHomeOwner;
    }
}

C# 6.0 (следующая версия):

public class Person
{
    public readonly string LastName { get; }
    public readonly string FirstName { get; }
    public readonly string MiddleName { get; }
    public readonly string Salutation { get; }
    public readonly string Suffix { get; }
    public readonly string StreetAddress { get; }
    public readonly string City { get; }
    public readonly string State { get; }
    public readonly bool IsFemale { get; }
    public readonly bool IsEmployed { get; }
    public readonly bool IsHomeOwner { get; }

    public Person (string lastName = "",
        string firstName = "",
        string middleName = "",
        string salutation = "",
        string suffix = "",
        string streetAddress = "",
        string city = "",
        string state = "",
        bool isFemale = false,
        bool isEmployed = false,
        bool isHomeOwner = false)
    {
        LastName = lastName;
        FirstName = firstName;
        MiddleName = middleName;
        Salutation = salutation;
        Suffix = suffix;
        StreetAddress = streetAddress;
        City = city;
        State = state;
        IsFemale = isFemale;
        IsEmployed = isEmployed;
        IsHomeOwner = isHomeOwner;
    }
}

C# 7.0 (планируемый синтаксис, раньше планировалось в 6.0):

public class Person(
    string lastName = null,
    string firstName = null,
    string middleName = null,
    string salutation = null,
    string suffix = null,
    string streetAddress = null,
    string city = null,
    string state = null,
    bool isFemale = false,
    bool isEmployed = false,
    bool isHomeOwner = false)
{
    public readonly string LastName { get; } = lastName;
    public readonly string FirstName { get; } = firstName;
    public readonly string MiddleName { get; } = middleName;
    public readonly string Salutation { get; } = salutation;
    public readonly string Suffix { get; } = suffix;
    public readonly string StreetAddress { get; } = streetAddress;
    public readonly string City { get; } = city;
    public readonly string State { get; } = state;
    public readonly bool IsFemale { get; } = isFemale;
    public readonly bool IsEmployed { get; } = isEmployed;
    public readonly bool IsHomeOwner { get; } = isHomeOwner;
}

Использование:

var person = new Person(
    lastName: "John",
    firstName: "Smith",
    city: "New York",
    state: "New Tork");

Ну вы поняли. trollface.jpg.to
Да нафиг нам в вашу обитель зла, у нас есть Scala!
Скала — это хорошо, но найти работу с ней — это та ещё задачка, в отличие от мейнстрима. Ну не любит её энтерпрайз.

Дотнет с шарпом со следующего релиза — FOSS, так что традиционный аргумент про обитель зла перестаёт работать. :-)
Но тут еще вопрос — а нужна ли контора, закрытая для новых технологий? Как бы на поиск работы можно смотреть совсем по-разному: или вот хочу в эту контору, нужно знать то-то и то-то, или хочу делать вот то-то на вот этом, вот конторы где это можно. Что касается интерпрайза, то есть уже вакансии в том же Luxoft на Scala, так что тут на месте дело не стоит.

Что касается открытости дотнета, формального открытия недостаточно, пройдут годы, прежде чем появится реальное oss комьюнити. А потом официальной версии под *nix пока нет. Очень странная технологий «добра» выходит, мы типа открытые, но работаем только на win, зашибись. Пока это все появится, глядишь и Scala станет мейнстимом :)
Вообще там и офф версия под *nix ожидается. Да и комьюнити есть. Не такой большой, но весьма бодрый)

Вот выдержка из статьи:
We are building a .NET Core CLR for Windows, Mac and Linux and it will be both open source and it will be supported by Microsoft. It'll all happen at github.com/dotnet.

Announcing .NET 2015 — .NET as Open Source, .NET on Mac and Linux, and Visual Studio Community
Ожидается и в продакшне — две разные разницы :)
Нет оснований не верить, что не будет офф версии. Особенно имея уже рабочий k runtime )
Дык а я и верю. Просто вот как появится, так и поговорим :)
Скала одновременно к сожалению и к счастью регулярно ломает обратную и прямую совместимость, хотя и обещает чуток стабилизироваться.
Вообще-то они обещают все еще сильней поломать :) И в этом ее сила. Может я не очень понятно выразился, я и не утверждаю что скалу примет интерпрайз, скорее стоит говорить о каком-то ограниченном применении. Я лишь говорю, что на интерпайзе свет клином не сошелся, мне он что со скалой, что без скалы не нужен. Есть куча клевых и популярных технологий, которые там не используют, и это лишь довод для меня там не работать. Но повод работать там, где все это используют :)
Потому и написал, что и к сожалению, и к счастью)

Они так часто ломают совместимость, т. к. живут на стандартной JVM. Это хорошо в плане скорости работы (т. к. хороший gc и jit из коробки) и динамичного развития самого языка.
Популярность языка — к сожалению, очень и очень важный фактор. Во-первых, не все живут в столицах, поэтому на весь город запросто может не быть ни одной компании, использующей ваши любимые технологии. Во-вторых, выбор — это сила. Из пяти компаний я выбираю или из сотни — это две большие разницы, как говорится.

Скалу в мейнстрим уже многие годы обещают, но прогресс незаметен. Так что в чудеса не верю. :)

С OSS комьюнити в дотнете всё в порядке. Уход дотнета в опен-сорс — это не резкий непродуманный рывок, а закономерный результат: мелкомягкие уже несколько лет к этому идут. Некоторые мелкомягкие библиотеки уже ушли в опен-сорс, некоторые популярные библиотеки от комьюнити получают поддержку мелкомягкими. Стеснения в выборе библиотек обычно не чувствую. Ну да, библиотек для парсинга командной строки — всего лишь 30, а не 130, как для джавы, но всё нужное есть.
Ну дык про популярность я же писал, тут две стратегии. Нет в одном городе, для меня это повод уехать из этого города, а не учить то, что в этом городе есть :) Тут смотря как к этому относиться.
Что касается комьюнити, то оно все-таки не в порядке, если говорить про не-интерпрайз. Если вы работате только в этой области, то вас ждет дивный новый мир :) Посмотрите на такие штуки, как reactive stack, typelevel stack. Посмотрите на такие платформы обработки данных, как spark. Потом интеграция с *nix это не мелочь, смотрите на такие штуки, как docker, vagrant, ansible, salt. Пока дотнет нормально не работает под никсы, вам эти технологии не доступны.
Энтерпрайз — это с любой стороны самый большой кусок разработки. Есть он — есть всё остальное. Соответственно, для меня это мера успеха платформы-технологии-языка.

Вы по части big data что ли? «Не-энтепрайз» — это для вас что?

Apache Spark — 1.0 только-только релизнулся, рано говорить про ограничения.
Docker — вижу статьи, как использовать с дотнетом.
Vagrant — аналогично.

Пока дотнет нормально не работает под никсы, вам эти технологии не доступны.

Моно уже давно работает нормально. Не слишком распространён — это да.
>Соответственно, для меня это мера успеха платформы-технологии-языка.

Ну вот видите, а для меня нет. Меня инерпрайз сам по себе не интересует, не смотря на распространенность.
Scala для энтерпрайза действительно не лучшая идея. Сложно и не однозначно во многих вопросах. Всегда считал энтерпрайз похожим чем-то на армию. Те же требования к коду, как в армии к технике: простота и однозначность в эксплуатации, надежность и результативность. Остальное (цена и расходы) не важно. Лично я очень жду Kotlin.
Так и есть. Просто мы начали разговор про удобство, закончили за упокой интепрайз :) Но опять же, есть дивный новый мир вне инерпрайза, где востребованы advanced технологии.
UFO just landed and posted this here
UFO just landed and posted this here
На дотнете намного больше вакансии?
Э… Вакансий и на джаве, и на шарпе на порядок больше, чем на скале. HH (msk): scala — 33, java — 777, c# — 470, python — 378, ruby — 109.
Какую страну, город ни выбирай, на каком сайте не ищи — скалы везде будет, как минимум, раз в 10 меньше, чем мейнстримовых языков. (Забавно, что моя оценка пальцем в небо «5 или 100» была настолько точна. :-D )

Искренне ваш, К.О.
Если не ставить вопрос об эффективности (==количестве мусора), то лучшая реализация билдера, которую я видел — в joda-time. Там сами доменные объекты (и изменяемые реализации, и неизменяемые) предоставляют методы типа A.withXXX(newValue) -> A. При этом изменяемые реализации возвращают this, а неизменяемые — новый экземпляр объекта с измененным полем. Отличный подход, кмк. Единственный его недостаток в том, что на каждое свойство создается новый объект (для неизменяемых реализаций), но и то, в горячем коде если нормально отработает inliner и escape analyzer, то бОльшая часть этих объектов должна быть скаляризована.
Этот подход используется во всех иммутабельных типах, например, в BigDecimal. И это очень хороший подход, но это не билдер. Смысл паттерна «билдер» в том, чтобы состояние необходимое для конструкции объекта вынести в отдельный объект билдера, который не будет использоваться в реальной работе. Это так же позволяет вынести какую-то логику конструирования. Вроде такого:
public class Person {

    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

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

    public static class Builder {
        private NameGenerator nameGenerator = new RandomNameGenerator();
        private Integer age;

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

        public Builder birthDate(Calendar date) {
            return age(Calendar.getInstance().get(Calendar.YEAR) - date.get(Calendar.YEAR));
        }

        public Builder name(String name) {
            return nameGenerator(new FixedNameGenerator(name));
        }

        public Builder nameGenerator(NameGenerator generator) {
            this.nameGenerator = generator;
            return this;
        }

        public Person build() {
            if (age == null) {
                throw new IllegalArgumentException();
            }

            return new Person(nameGenerator.generate(), age);
        }
    }

    public static void main(String[] args) {
        Person person = Person.builder().age(10).build();
    }
}
В целом да, вы правы. Но тогда задача, которую ставит автор (не дублировать само описание состояния — поля) — не решаема, потому что билдер принципиально имеет другое состояние. Но на практике-то мы все знаем, что в половине случаев состояние вообще совпадает полностью, и все отличие в мутабельности/иммутабельности, еще в половине от оставшейся половины, опять же, состояние совпадает, и разница в дополнительных проверках, которые делает билдер, да более удобном API — fluent, специальные группы методов под конкретные паттерны использования, и т.п. И остается не так уж много случаев, когда билдер реально какую-то нетривиальную логику реализует, и нетривиальное состояние имеет. Поэтому мысль как-то упростить работу с наиболее частым случаем недурна — хотя, увы, я для джавы не вижу хорошей реализации

>Смысл паттерна «билдер» в том, чтобы состояние необходимое для конструкции объекта вынести в отдельный объект билдера, который не будет использоваться в реальной работе. Это так же позволяет вынести какую-то логику конструирования.

Рассуждения об истинном смысле каких-то шаблонов мне кажутся не очень осмысленными, потому что шаблон — это только идея.
Например, по вашему же описанию «какая-то дополнительная логика создания» вполне себе вписывается в тот метод, что я описываю. «Отдельное состояние» — нет, но это нужно реже. Если я навешу на свой иммутабельный объект два интерфейса — Builder, в который вынесу все fluent withXXX() методы, и ReadableXXX где только на чтение доступ, то я смогу передавать свой объект в методы, которые принимают только Builder, и знать не знают, как он внутри устроен. С точки зрения таких методов — мой объект будет натуральным построителем, так ведь?
проблема в том, интерфейсы у билдера и объекта разные, и придется заниматся дублированием методов, рефакторить тоже вручную. Можно только сделать динамическую реализацию интерфейса билдера, но тогда ошибки будут уже не во время компиляции, а при запуске.

т.е. написать что-то вроде:
public interface IBuilder<T> {
    public T build();
}

public interface IPersonBuilder extends IBuilder<Person>
{
    public IPersonBuilder age(int value);
}

Builder.newBuilder(IPersonBuilder.class).age(10).build();


реализация которого будет сгенерена динамически, c проверкой существования полей.
Задач buider решает много, лично я создаю его для следующих целей
  • На вход конструктору подается большое количество объектов, порядок которых легко перепутать
  • Я хочу сделать все поля класса неизменяемыми, чтобы немного облегчить себе жизнь в многопоточный среде (в идеале это все поля final)
  • В конструкторе происходят какие то действия, которые могут вызвать исключения ( в builder это вынесется в set метод поля) — например на вход подается json и не факт что там есть нужное поле, да и вообще парсить его в конструкторе я считаю некрасиво
  • Еще можно придумать случай когда внутри конструктора меняются стратегиии инициализации полей, тогда можно это вынести в set методы builder'а и еще задавать стратегии


И пример автора не одну из этих задач не решает
Sign up to leave a comment.

Articles