Статья о реализации паттерна Builder с проверкой на уровне компиляции, реализованного с помощью параметрического полиморфизма. В ней мы поговорим о том, что такое полиморфизм, каким он бывает. Как устроена магия «оператора» =:= в scala, можно ли повторить ее в java и как используя эти знания реализовать Builder, не допускающий неполной инициализации создаваемого объекта.

Когда в системе возникает сущность с множеством свойств, возникает проблема с ее конструированием. Многословный конструктор или множество setter-ов? Первое выглядит громоздким, второе не безопасно: можно легко упустить вызов метода инициализации важного свойства. Для решения этой проблемы часто прибегают к паттерну Builder.

Паттерн builder решает две задачи: во-первых разделяет алгоритм создания(инициализации) объекта от деталей его(объекта) реализации, во-вторых упрощает сам процесс создания:

UrlBuilder()
    .withSchema("http")
    .withHost("localhost")
    .withFile("/")
    .build()

Остается вопрос: как реализовать builder так, чтобы он не допускал не полной инициализации объекта?

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

Следующее что приходит на ум, это StepBuilder — реализация builder, в которой для каждого нового шага описан свой отдельный класс/интерфейс. Недостатком такого решения является крайняя избыточность реализации.

Несколько иной подход практикуют сторонники scala. Для проверки законченности конфигурирования объекта в scala используется параметрический полиморфизм:

trait NotConfigured
trait Configured

class Builder[A] private() {
  def configure(): Builder[Configured] = new Builder[Configured]
  def build()(implicit ev: Builder[A] =:= Builder[Configured]) = {
    println("It's work!")
  }
}

object Builder {
  def apply(): Builder[NotConfigured] = {
    new Builder[NotConfigured]()
  }
}

Builder()
  .configure() // без вызова этого метода компилятор поругается!
  .build()

Если при использовании такого builder опустить один метод configure() и вызвать метод build(), компилятор выдаст ошибку:

scala> Builder()./*configured()*/.build()
Error:(_, _) Cannot prove that Builder[NotConfigured] =:= Builder[Configured].

Контролем типа в данном примере занимается «оператор» =:=. Запись A =:= B говорит о том, что параметрический (generic) тип A должен быть равен типу B. Мы еще вернемся к данному примеру и разберем магию, с помощью которой компилятор scala отлавливает незавершенное состояние инициализации создаваемого объекта. А пока вернемся в мир более простой и понятной java и вспомним что такое полиморфизм.

В ООП полиморфизм — свойство системы, позволяющее использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта. Но то, что мы привыкли называть полиморфизмом в ООП, только частный случай полиморфизма — полиморфизм подтипов. Другим видом полиморфизма является параметрический полиморфизм:
Параметрический полиморфизм позволяет определять функцию или тип данных обобщённо, так что значения обрабатываются идентично вне зависимости от их типа. Параметрически полиморфная функция использует аргументы на основе поведения, а не значения, апеллируя лишь к необходимым ей свойствам аргументов, что делает её применимой в любом контексте, где тип объекта удовлетворяет заданным требованиям поведения.

Примером может служить функция <N extends Number> printNumber(N n). Эта функция выполняется только для аргументов классов-наследников Number. Обратите внимание на то, что компилятор способен проконтролировать соответствие типа переданного аргумента всем ожиданиям параметризированной функции и выдать исключение в случае вызова функции с некорректным аргументом:

java> printNumber("123")
Error:(_, _) java: method printNumber ... cannot be applied to given types;
required: N
found: java.lang.String
...

Это может натолкнуть на мысль о функции build, которая определена только для полностью сконфигурированного экземпляра builder. Но остается открытым вопрос: как объяснить это требование компилятору?

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

interface NotConfigured {}
interface Configured {}

static class Builder<A> {
    static Builder<NotConfigured> init() { 
        return new Builder<>();
    }
    private Builder() {}

    public Builder<Configured> configure() {
        return new Builder<>();
    }

    // первая попытка
    public <T extends Builder<Configured>> void build() { 
        System.out.println("It's work!");
    }
    
    public static void main(String[] args) {
          Builder.init()
            // .configure()                           // вызов конфигурации опущен,
            .<Builder<Configured>>build()  // но вызов метода build все еще доступен
    }
}

Зайдем с другой стороны: потребуем при вызове метода build доказательство того, что текущий экземпляр полностью сконфигурирован:

public void build(EqualTypes<Configured, A> approve)
...
class EqualTypes<L, R> {}

Теперь чтобы вызвать метод build мы должны передать экземпляр класса EqualTypes такой, в котором тип L равен Configured, а тип R равен типу A, определенному в текущем экземпляре класса Builder.

Пока от такого решения проку мало, достаточно просто опустить тип при создании экземпляра EqualTypes и компилятор позволит нам вызвать функцию build:

public static void main(String[] args) {
    Builder.init()
            // .configure()
            .build(new EqualTypes())
}

Но если объявить параметризированный фабричный метод такой, который принимал бы некоторый тип T и создавал экземпляр класса EqualTypes<T, T>:

static <T> EqualTypes<T, T> approve() {
    return new EqualTypes<T, T>();
}

и вызывая метода build передавать в него результат работы функции approve, мы получим долгожданный результат: компилятор будет ругаться, если опустить вызов метода configure:

java>Builder.init()./*configured()*/.build(approve())
Error:(_, _) java: incompatible types: inferred type does not conform to equality constraint(s)
inferred: NotConfigured
equality constraints(s): NotConfigured,Configured

Дело в том, что к моменту вызова метода build, параметрический тип A класса Builder имеет значение NotConfigured, тк именно с таким значением создается экземпляр в результате вызова метода init. Компилятор не может подобрать такой тип T для функции approve, чтобы он с одной стороны был равен Configured, как того требует метод build, и с другой стороны NotConfigured как параметрический тип A.

Теперь обратите внимание на метод configure — он возвращает такой экземпляр класса Builder, в котором параметрический тип A определен как Configured. Т.е. при правильной последовательности вызова методов компилятор сможет вывести тип T как Configured и вызов метода build пройдет успешно!

java>Builder.init().configured().build(approve())
It's work!

Осталось добиться того, чтобы единственным способом создать экземпляр класса EqualTypes остался метод approve, но это уже задание на дом.

В качестве типа T может выступать более сложный тип, например Builder<A>. Сигнатура метода build может быть изменена на несколько более громоздкую:

void build(EqualTypes<Builder<Configured>, Builder<A>> approve)

Преимущество такого подхода заключается в том, что если понадобится добавить новый обязательный метод, достаточно будет завести для него новый generic параметр.

Пример UrlBuilder
interface Defined {}
interface Undefined {}

class UrlBuilder<HasSchema, HasHost, HasFile>  {

  private String schema = "";
  private String host = "";
  private int port = -1;
  private String file = "/";

  static UrlBuilder<Undefined, Undefined, Undefined> init() {
    return new UrlBuilder<>();
  }

  private UrlBuilder() {}
  private UrlBuilder(String schema, String host, int port, String file) {
    this.schema = schema;
    this.host = host;
    this.port = port;
    this.file = file;
  }

  public UrlBuilder<Defined, HasHost, HasFile> withSchema(String schema) {
    return new UrlBuilder<>(schema, host, port, file);
  }

  public UrlBuilder<HasSchema, Defined, HasFile> withHost(String host) {
    return new UrlBuilder<>(schema, host, port, file);
  }

  public UrlBuilder<HasSchema, HasHost, HasFile> withPort(int port) {
    return new UrlBuilder<>(schema, host, port, file);
  }

  public UrlBuilder<HasSchema, HasHost, Defined> withFile(String file) {
    return new UrlBuilder<>(schema, host, port, file);
  }

  public URL build(EqualTypes< UrlBuilder<Defined, Defined, Defined>, UrlBuilder<HasSchema, HasHost, HasFile>> approve) throws MalformedURLException {
    return new URL(schema, host, file);
  }

  public static void main(String[] args) throws MalformedURLException {
    UrlBuilder
            .init()
            .withSchema("http")    // пропуск любого
            .withHost("localhost")  // из этих методов
            .withFile("/")         // приведет к исключению при компиляции!
            .build(EqualTypes.approve());
  }
}


Вернемся к примеру на scala и посмотрим на то, как устроен «оператор» =:=. Здесь стоит заметить, что в scala допустима инфиксная форма записи параметров типа, что позволяет записать конструкцию вида =:=[A, B] как A =:= B. Да-да! На самом деле =:= — никакой не оператор, это абстрактный класс объявленный в scala.Predef, очень похожий на наш EqualTypes!

@implicitNotFound(msg = "Cannot prove that ${From} =:= ${To}.")
sealed abstract class =:=[From, To] extends (From => To) with Serializable
private[this] final val singleton_=:= = new =:=[Any,Any] { def apply(x: Any): Any = x }
object =:= {
   implicit def tpEquals[A]: A =:= A = singleton_=:=.asInstanceOf[A =:= A]
}

Разница лишь в том, что вызов функции approve (а точнее ее аналога tpEquals) компилятор скалы подставляет автоматически.

Получается, что привычное оперирование типами в scala (речь идет о применении конструкций =:=, <:<) вполне применимо в java. Но, тем не менее, механизм implicit предусмотренный в scala делает подобное решение более лаконичным и удобным.

Еще одним преимуществом реализации описанного подхода в scala является аннотация @implicitNotFound, которая позволяет управлять содержимым исключения при компилировании. Эта аннотация применима к классу, экземпляр которого не может быть найден для неявной подстановки компилятором.

Плохая новость в том, что вы не можете поменять текст ошибки для конструкции =:=, а хорошая — теперь вы можете легко создать собственный аналог с нужным вам сообщением!

object ScalaExample extends App {

  import ScalaExample.Builder.is
  import scala.annotation.implicitNotFound

  trait NotConfigured
  trait Configured

  class Builder[A] private() {

    def configure(): Builder[Configured] = new Builder[Configured]

    def build()(implicit ev: Builder[A] is Builder[Configured]) = {
      println("It's work!")
    }
  }

  object Builder {

    @implicitNotFound("Builder is not configured!")
    sealed abstract class is[A, B]
    private val singleIs = new is[AnyRef, AnyRef] {}
    implicit def verifyTypes[A]: A is A = singleIs.asInstanceOf[is[A, A]]

    def apply(): Builder[NotConfigured] = {
      new Builder[NotConfigured]()
    }
  }

  Builder()
    .configure() // без вызова этого метода компилятор поругается:
    .build()     // Builder is not configured!
}

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

Что касается Java, развитие языка не стоит на месте, язык меняется в лучшую сторону, вбирая в себя решения и конструкции из других языков. При этом не всегда стоит ждать нововведений от авторов языка, некоторые подходы и решения можно пробовать заимствовать уже сейчас.
Поделиться с друзьями
-->

Комментарии (22)


  1. reforms
    06.03.2017 14:37

    У меня 2 вопроса:
    1. Стоят ли все эти усилия того, чтобы просто внимательно отработать на get/set методах при создании объекта (при код ревью или самопроверке)?
    2. Не является ли такой подход более многословным, нежели JUnit тест мест создания таких объектов?
    Спасибо.

    Я спрашиваю, просто очень много на хабре пишут про билдере в ключе создания объектов, но вот в моей 8 летней практике я ни разу не встречал проблем при работе с такими объектами (get/set).
    Тем не менее сам патерн билдер я использовал немного в другом ключе — создание PrepareStatement для разных версий и типов АБС при интеграции данных.


    1. dokwork
      06.03.2017 14:48
      +2

      Если смотреть с точки зрения использования самого builder-а, то он может казаться избыточным. Тем более при наличии модульных тестов. Но если вы пишете builder с тем, чтобы его использовали другие люди, то Ваши усилия могут окупиться сполна


  1. habradante
    06.03.2017 15:57

    Все вроде бы хорошо, но как быть, когда вызвать надо не только configure, но и configure2, configure3 и т.д.?


    1. dokwork
      06.03.2017 16:58

      Посмотрите пример с UrlBuilder (под спойлером), там как раз несколько обязательных к конфигурирование свойств.


  1. GreyCat
    06.03.2017 19:49

    Вообще, если с точки зрения терминологии — по-моему, методы с в Java всю жизнь называли generic methods, а не "параметрический полиморфизм".

    Предложенный метод забавен, но упирается в то, что по сути это вызов approve() руками, вместо implicit-вызова, которое генерирует type inferring в Scala.


    Что касается Java, развитие языка не стоит на месте, язык меняется в лучшую сторону, вбирая в себя решения и конструкции из других языков.

    Субъективно, Java как сам язык имеет дикое количество родовых травм и при попытке добавлять туда какие-то современные элементы функциональщины а ля Scala прямо все сурово трещит по швам, в первую очередь в районе системы типов. По крайней мере из того, что я вижу — программистов на "современной Java" сейчас уже сильно меньше, чем программистов на той же Scala. Приличная масса до сих пор сидит в лучшем случае на 7 (а то и на 6), или, даже если меняли компилятор — все равно в code base используют все те же практики 15-летней давности.


    JVM же как раз развивается полезно, но как-то странно. Появляется масса интересных оптимизаций, JVM 8 действительно стала прямо летать на куче всяких интересных задач, но опять же — радикальных изменений в районе той же системы типов — так и не происходит. Фактически, JVM как ничего не знал о generics — так и не знает. Как была одна куцая инструкция instanceof — так, фактически и остается. Как плодятся классы на каждый чих для функциональных проявлений — так и плодятся, разве что благодаря более эффективным GC стало не так заметно.


    1. sshikov
      06.03.2017 21:00

      >Приличная масса до сих пор сидит в лучшем случае на 7 (а то и на 6), или, даже если меняли компилятор — все равно в code base используют все те же практики 15-летней давности.

      Да ладно. У меня обратные впечатления. Сам с удовольствием перешел сначала на 7, а потом на 8, причем в первом случае реальные улучшения были минимальны. А те, котому это интересно (или делать нечего, или и то и другое вместе ;) охотно пробуют вообще все новинки, а также кложу, скалу, котлин и пр. языки, все какие есть.

      Те же, кто сидит на старых версиях, как правило вынуждены. Например, у нас тут рядом есть небольшая команда, человек 10, которые привязаны к Jboss Fuse, и эта купленная много лет назад версия, не работает на Java 8. А смигрировать большой проект — это не так просто. А так бы с радостью давно бы перешли.


      1. GreyCat
        06.03.2017 21:12

        Сам с удовольствием перешел сначала на 7, а потом на 8, причем в первом случае реальные улучшения были минимальны.

        Ради интереса и статистики — что из 8 вы используете? Какой размер codebase у мигрированных проектов?


        Возможно, что у меня сильно сдвинутая выборка — но я наблюдаю, что почти все, что сейчас делается новое-модное-интересное в районе JVM — делается на Scala или, реже, Clojure. Это всякие нагруженные вычисления, статистика, кластеризация, поисковые движки, всякие агентные системы, мессаджинг, игры с массивным количеством клиентов, one-page сайты с хорошей интеграция frontend/backend и т.п.


        Java я вижу почти исключительно в командах, занимающихся системами, которые зачастую существуют уже не первый десяток лет — банки, EE, какие-то относительно старые существующие веб-порталы, либо что-то, что вынуждено с таким интегрироваться. Там даже Spring + Hibernate зачастую считается чем-то прям новым и хипстерским. И, да, зачастую Java 6 и все.


        1. sshikov
          06.03.2017 21:38

          Ну так JavaEE (в виде например WAS) — это и есть тот фактор, который сдерживает. Чтобы поддерживалась Java 8, нужно купить новую версию WAS, а кто и зачем это будет делать, если старая работает нормально? Это ровно таже причина, что я наблюдал с Fuse. Знаю, что до сих пор сидят местами на EJB 2. Но это на мой взгляд дурь — если бы смигрировали, получили бы приличные преимущества.

          >Ради интереса и статистики — что из 8 вы используете? Какой размер codebase у мигрированных
          проектов?

          Я использовал в основном стримы, и лямбды. Проект маленький — где-то три человеко месяца, может чуть больше/чуть меньше.

          Это (инвестиционный) банк. Кстати, был период тут (уже в другом проекте), когда народ увлекался, и писал на чем хотел. Clojure например. Сейчас наблюдается тенденция вернуться обратно на Java, потому что это с одной стороны, инфраструктура, и клиенты у нее разные, и иметь ее на Java — значит проще взаимодействовать. А с другой — проще поддерживать, потому что любитель кложи был один, он ушел — и опаньки.


          1. GreyCat
            06.03.2017 22:24

            Но это на мой взгляд дурь — если бы смигрировали, получили бы приличные преимущества.

            Субъективно, профит получается, если выкидывать EE отовсюду и переделывать все на что-то более адекватное и современное. Но на это, как правило, ни верхи, ни низы пойти не могут: верхи — потому, что дорого, а низы — потому что зачем терять работу?


            Сейчас наблюдается тенденция вернуться обратно на Java

            Я такое года 3-4 назад наблюдал. Именно когда "сбегали" одиночки в инициативном порядке и, да, тогда некоторые огребали "любитель кложи был один". Сейчас это время прошло, народ целенаправленно и осознанно начинает новые проекты на Scala или Clojure, и программисты внезапно набираются, и так далее. И implicit'ы со scalaz уже не лепят везде, где только можно, просто потому что "круто".


            1. sshikov
              06.03.2017 23:01

              Про EE — совершенно необязательно. При переходе на EJB 3 было серьезное упрощение почти всего. Это примерно 2008-2009. С одновременным облегчением контейнеров.

              С тех пор, если вы смигрировали, смысла уходить куда-то уже не наблюдается. Хотя начинать что-то новое не на EE — может быть и проще. Но опять же — совсем не сильно. Что адекватное-то? Микросервисы? Так я готов поспорить, их преимущества зачастую сомнительны, потому что EE — это и есть контейнер для микросервисов. Как впрочем и OSGI.

              А насчет остального… ну как у скалы, так и у кложи, есть некоторые недостатки. В каждом случае свои. И достоинства в общем тоже в каждом случае свои, разные. Чем дольше живет язык — тем недостатки и достоинства народу становятся яснее, это же понятно. Ну и делать «потому что круто» сейчас вот на кложе уже не круто — в моде уже другие.

              И у нас таки есть целенаправленный уход от кложи — потому что когда-то применили не по делу. Я не говорю что это общая тенденция — но факты наблюдаю лично.


              1. GreyCat
                06.03.2017 23:53
                +1

                Что адекватное-то? Микросервисы? Так я готов поспорить, их преимущества зачастую сомнительны, потому что EE — это и есть контейнер для микросервисов. Как впрочем и OSGI.

                В EE, по современным меркам, плохо, по-моему, примерно все. Неудобная (потому что бинарная и требует весьма специфичных инструментов для отладки и контроля) и медленная (потому что о каких-то серьезных нагрузках, promises и нетредовых архитектурах там и близко не слышали) messaging bus. Жесткая привязка к одной платформе — я вот сильно сомневаюсь, что кто-то в production будет сажать на шину (т.е. реализовать эти самые микросервисы) на чем-то, отличном от Java. Отсутствие простых базовых сервисов, собственно, "как сервис". Захотите какое-нибудь готовое хранилище документов от Amazon / Google, или, там, не знаю, дергать cloud'ное API для генерации картинок > в лучшем случае, все закончится проектом эдак на человекомесяц-два по написанию монструозной обертки над этим для EE. Там, где разработчик на Ruby или Node.js напишет две-три строчки и потратит 15 минут (из которых 14 будут написанием тестов), на EE это займет в десятки и сотни раз больше времени. Современный мир — гетерогенен и, хочется нам того или нет, SaaS/PaaS/IaaS зачастую дают гигантские преимущества.


                Даже те сферы, где EE типично было удобно применять (типа автоматизации бизнес-процессов), сейчас дико подминают под себя, скажем, тот же Salesforce, Dynamics и SAP.


                1. sshikov
                  07.03.2017 09:14
                  -1

                  Хм. Ваши претензии к EE выглядят странно, если не сказать больше. Ну кроме разве что нагрузки — хотя утверждать что в мире enterprize не слышали о серьезных нагрузках — это сильно. Но тут я просто не в теме, потому что реально нагруженных систем на этом не делал.

                  Но в целом вы какую-то очень древнюю JavaEE себе представляете, по-моему.

                  Привязка к платформе? В каком месте? Если мне будет очень зачем-то нужно написать кусок на чем-то отличном от Java — я его прекрасно напишу на почти любом из пары десятков приличных языков. EE этому перпендикулярно.

                  Про cloud API вообще не понял посыла. Ну так, для начала — и Ruby (в виде JRuby), и javascript (в виде Rhino или Nashorn) в мире JVM, и в JavaEE тоже есть. Если разработчик Node.js напишет две-три строчки — что помешает мне написать те же две строчки, на тех же языках? JavaEE это легкое и простое API, уже лет 8 как. Там нет практически никаких ненужных накладных расходов. Вы просто берете — и пишете сервис. Который потом деплоите в любой из контейнеров, и контейнер дальше раздает его наружу, по любому из множества протоколов, включая древнюю corba. И обеспечивает вам транзакции, и прочие плюшки, по потребности.


                  1. GreyCat
                    07.03.2017 11:46
                    +1

                    хотя утверждать что в мире enterprize не слышали о серьезных нагрузках — это сильно.

                    "Мир enterprise" до сих пор работает 5 дней в неделю по 8 часов с перерывом на обед. "Мир enterprise" до сих пор мыслит оперднями и на полном серьезе складывает данные в Oracle. Единицы миллиардов транзакций в год — это до сих пор национальный потолок даже для крупных банков и транспортных систем. Где здесь нагрузки?


                    Привязка к платформе? В каком месте?

                    Отлично, вот есть у меня, допустим, система документооборота, построенная на базе EE. И вот в компании захотелось, внезапно, обеспечить интеграцию с каким-нибудь готовым существующим внешним решением по GPS-трекингу своего парка грузовых автомобилей с водителями, или, например, управления очередями печати в офисных принтерах. Что будет дальше происходить? В моем представлении, в лучшем случае будет происходить написание пары десятков врапперов над этим API. В худшем (как это обычно происходит) — просто никто не будет заморачиваться, подгрузят каких-нибудь библиотек в основной компонент и подергают вызовами оттуда. А потом, когда это понадобится в каком-то другом компоненте — добавят и туда, и подергают еще и оттуда.


                    1. sshikov
                      07.03.2017 12:26

                      Ерунду вы какую-то говорите, пардон. Мир enterprise — это например биржевая роботизированная торговля, которая торгует тогда, когда работают биржи. Т.е. фактически — круглосуточно, потому что Токийская биржа открывается примерно тогда, когда закрывается Нью-Йоркская.

                      Число транзакций? А вы много где видели скажем Терадату помимо банков? Я вам подскажу — еще сотовые операторы обычно кроме банков ее держат. Объемы входящих маркет данных тоже вполне себе немаленькие.

                      А про второй абзац снова не понял. EE-то тут каким боком? Для EE не нужно никаких врапперов для сервиса GPS трекинга, вы откуда это взяли? JavaEE — это про организацию компонентов внутри контейнера. Т.е. как будет выглядеть скажем сервис, причем это вовсе не догма — можно делать и не так, причем довольно переносимо.

                      К тому, как вы будете общаться с внешним сервисом, будь то GPS или принтеры, это никакого отношения ровным счетом не имеет. То есть ваще никакого.


                      1. GreyCat
                        07.03.2017 12:52

                        Мир enterprise — это например биржевая роботизированная торговля, которая торгует тогда, когда работают биржи.

                        Окей, покажите мне хоть одну биржу или трейдерский софт на EE ;)


                        А вы много где видели скажем Терадату помимо банков? Я вам подскажу — еще сотовые операторы обычно кроме банков ее держат. Объемы входящих маркет данных тоже вполне себе немаленькие.

                        Последний раз когда я смотрел на Teradata DWH, он был написан на C++. Ну и да, по нынешним временам я бы их в пример приводить не стал, дела у них идут относительно так себе.


                        Одного сотового оператора наблюдаю прямо сейчас. Ничего особенно впечатляющего — ни в количестве транзакций, ни в размерах в байтах. Терадата у них живет исключительно в BI и в сильно урезанном виде. Основной внутренний рабочий DWH — Splunk.


                        Среднестатистический bigdata-стартап, считающий какие-нибудь рекомендации или рекламу, обрабатывает в 10-100 раз больше данных в сутки.


                        К тому, как вы будете общаться с внешним сервисом, будь то GPS или принтеры, это никакого отношения ровным счетом не имеет. То есть ваще никакого.

                        А вот микросервисная архитектура, с которой мы начинали, вполне себе имеет.


                        1. sshikov
                          07.03.2017 13:27

                          >А вот микросервисная архитектура, с которой мы начинали, вполне себе имеет.

                          Я вам еще раз повторю — JavaEE это не про внешние API. Как вы с ними будете работать — исключительно ваши трудности. Хотите — врапперов напишите, хотите так потребляйте. JavaEE тут ни при чем, никаким боком. Поэтому какие у вас тут претензии к JavaEE, я так и не понял.


                1. sshikov
                  07.03.2017 09:20

                  Хотя насчет высокой нагрузки — я пожалуй соглашусь. Точнее — насчет масштабирования.

                  Это у JavaEE изначально не было заложено, соответственно, развернуть еще парочку или десяток копий сервиса, которые на справляются с нагрузкой — это может быть нетривиальная задача.


  1. sshikov
    06.03.2017 20:54
    +1

    Все классно, только я эту конструкцию видел много лет назад. Это ни в коем случае не в упрек автору, просто по-первых, разные люди приходят к одним и тем же идеям независимо, а во-вторых, такое решение было возможно уже с момента появления generics, т.е. с версии 1.5.


  1. kemsky
    08.03.2017 03:41

    По сути дела надо проверить являются ли введенные параметры валидными, это в сто раз проще всего, что тут написано и на 100% закрывает задачу.


    PS. Как безопасно написать такой билдер:


            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context)
                    .setSmallIcon(getType().getIconRes())
                    .setTicker(getMessage())
                    .setOngoing(true)
                    .setCategory(Notification.CATEGORY_PROGRESS)
                    .setColor(context.getResources().getColor(R.color.main))
                    .setVisibility(Notification.VISIBILITY_PUBLIC)
                    .setWhen(new Date().getTime())
                    .setShowWhen(true)
                    .setPriority(Notification.PRIORITY_HIGH)
                    .setProgress(getTotal(), getProgress(), getIndeterminate())
                    .setContentTitle(context.getResources().getString(R.string.app_name))
                    .setContentText(getMessage())
                    .setContentIntent(getProgressIntent(context));


    1. dokwork
      08.03.2017 11:31

      Всякому решению свое применение. Вереница условий с проверкой каждого параметра из такого большого списка будет не многим лучше выглядеть чем куча generic параметров и все еще поздно (только кода программа запущена, и выполнена ветка исполнения касающаяся билдера) давать фидбек. Тут дело уже в другом — я бы расстроился, если бы в используемой библиотеке мне пришлось инициализировать такого монстра. Возможно части параметров из примера можно выставить значения поумолчанию, часть сгруппировать.


  1. LSD
    09.03.2017 16:10

    Как говорилось на одной известной картинке: «но зачем?»
    Все примеры, которые я видел, применения по делу, не требовали обязательного вызова всех методов. Как правило речь идет о вызове лишь части методов, иногда они даже бывают взаимоисключающие.


    1. dougrinch
      09.03.2017 16:39

      А мне бы хотелось compile-time проверку для сложных билдеров. Главная проблема предложенного решения — добавлять еще один метод очень сложно (надо кучу дженериков поправить). Но это решается вынесением билдера в какой-нибудь lombok в джаве или макросы в скале.


      Так здесь же важна сама идея. Опциональные/взаимоисключающие методы сюда уже просто добавляются. Правда, выглядеть хорошо это будет только на скале. В джаве надо будет писать по своему approve для каждого метода.