Думали про такой вариант. Неплохой, но отпал т.к.
1) ухудшается API discoverabilitiy — надо знать какой из apply дёрнуть чтобы настроить firstName, вместо .builder().firstName()
2) если наследование глубже 1 класса, то вообще страшно выходит
3) лямбду нельзя на инстанс "забиндить"
Самое забавное — в моём личном опыте сложней всего для поддержки проекты доставались именно от товарищей, которые пропагандируют "правильные" способы, пишут на Java как на Scala, а ночью под подушкшой читают куски Haskell кода.
Так что думаю тут знаний "мудрейших" не достаточно, и надо уметь их применять там, где оно уместно, и если что-то можно сделать проще — то почему бы и нет?
как пользователь, мне б это не понравилось, и вот почему:
1) API discoverability — если я хочу настроить порт, то мне надо знать наперёд в каком из билдеров этот метод для настройки порта указан
2) многословность — в вашем примере вы по сути дела устанавливаете 3 параметра, но при этом код нагружен .builder(), .build() и их друзьями
3) результат .build() должен содержать параметры всех "билдеров" (к сожалению в вашем примере это невозможно продемонстрировать), и, если это был билдер BarBuilder, то результат должен быть знать о свойствах Bar, а не только Foo
Продублирую мой ответ из твиттера:
SuperBuilder работает только если код и сторонние библиотеки к этому коду используют Java и Lombok. В нашем случае это не всегда так.
Допустим, я прочитал Ваш код. Но он не отвечает на вопрос! Как сделать удобный API для всего этого, чтобы пользователям было удобно?
Вас сразу выдаёт то, что вы приводите примеры объявления классов, а не пример их использования.
Когда мы у себя проектируем API наших DSL, мы начинаем с "как пользователь, я хочу использовать это вот так", а потом ищем варианты как это реализовать, на грани с возможностями языка, с учетом наших пользователей. А уже потом думаем, чтобы это ещё было поддерживаемо и читаемо.
Оно потому и "подойти иначе". Мы долго боролись за chaining, что забыли что есть другие способы, и что без него можно сделать вполне читаемый вариант :)
Это примерно то, что у нас сейчас (и в статье описано в секции про generic версию, только без 2х классов).
Такой подход имеет место быть (так например работает SuperBuilder в Lombok на сколько знаю), но, к сожалению, он очень тяжело даётся контрибьюторам в проект, особенно рекурсивные конструкции вида <SELF extends MyClass>.
Это, кстати, очень интересная тема. Одно дело — умные книжки про как можно и нельзя, а другое — как потом с таким кодом работать, особенно в OSS, где каждый контрибьютор важен.
Разработчики Testcontainers тоже не глупые и тоже разные книжки читали, но, как и во всём — лучшее враг хорошего :)
там внизу в комментарии есть problem definition. Ваш "идеальный" код никто не станет использовать, потому что он громоздкий и неудобный (по крайней мере в той библиотеке что я описываю).
Поэтому я и предлагаю переубедить, с примерами кода, иначе ваши комментарии выглядят как "я прочитал книжку, вы всё делаете неправильно, а я — умный".
Есть проект — Testcontainers. В нём есть базовый класс GenericContainer.
Есть наследники типа KafkaContainer.
Есть даже промежуточные наследования: MySQLContainer -> JDBCContainer -> GenericContainer.
Должна быть возможность их конфигурировать, чтобы удобно, красиво и вот это вот всё.
Как бы вы решили эту проблему на ровном месте? :)
Я там в конце статьи попытался написать "не стоит использовать везде", но такое стоит повторять — этот трюк не везде применим и может сделать больно в неправильных местах (capturing, serialization, classloading, вот это вот всё), особенно в production коде.
А вот для Testcontainers пока что выглядит очень даже норм :) Но всё ещё думаем...
Такой подход реализуется аннотацией
@Wither
в Lombok-е, но, к сожалению, не подходит для унаследованных сущностей.Думали про такой вариант. Неплохой, но отпал т.к.
1) ухудшается API discoverabilitiy — надо знать какой из apply дёрнуть чтобы настроить firstName, вместо .builder().firstName()
2) если наследование глубже 1 класса, то вообще страшно выходит
3) лямбду нельзя на инстанс "забиндить"
Я готов все свои карма поинты обменять на плюсы к Вашему комментарию, это просто прекрасно!
мы это конечно же добавим в будущем, просто хотел поделиться быстрым workaround-ом :)
параметров может быть не 3, а 30.
LocalStackContainer
наследуется же отGenericContainer
, можно любую env variaible указать с помощью.withEnv("FOO", "BAR")
.Самое забавное — в моём личном опыте сложней всего для поддержки проекты доставались именно от товарищей, которые пропагандируют "правильные" способы, пишут на Java как на Scala, а ночью под подушкшой читают куски Haskell кода.
Так что думаю тут знаний "мудрейших" не достаточно, и надо уметь их применять там, где оно уместно, и если что-то можно сделать проще — то почему бы и нет?
как пользователь, мне б это не понравилось, и вот почему:
1) API discoverability — если я хочу настроить порт, то мне надо знать наперёд в каком из билдеров этот метод для настройки порта указан
2) многословность — в вашем примере вы по сути дела устанавливаете 3 параметра, но при этом код нагружен
.builder()
,.build()
и их друзьями3) результат
.build()
должен содержать параметры всех "билдеров" (к сожалению в вашем примере это невозможно продемонстрировать), и, если это был билдер BarBuilder, то результат должен быть знать о свойствах Bar, а не только Fooв Spring Security норм, но им можно — у них не используется возвращаемый результат их DSL :)
Но даже если и адаптировать его под этот случай — DSL становится сложней читать из-за обилия
.and()
Внезапно то как!
Мне удобно было бы вообще не иметь public API. Нет API — нет проблем. Клаааас.
Продублирую мой ответ из твиттера:
SuperBuilder работает только если код и сторонние библиотеки к этому коду используют Java и Lombok. В нашем случае это не всегда так.
Я до сих пор удивлён что никто не упомянул трюк с
.and()
как это делает Spring Security:https://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#jc-httpsecurity
Ух ты как Java изменилась при Собянине
Допустим, я прочитал Ваш код. Но он не отвечает на вопрос! Как сделать удобный API для всего этого, чтобы пользователям было удобно?
Вас сразу выдаёт то, что вы приводите примеры объявления классов, а не пример их использования.
Когда мы у себя проектируем API наших DSL, мы начинаем с "как пользователь, я хочу использовать это вот так", а потом ищем варианты как это реализовать, на грани с возможностями языка, с учетом наших пользователей. А уже потом думаем, чтобы это ещё было поддерживаемо и читаемо.
Оно потому и "подойти иначе". Мы долго боролись за chaining, что забыли что есть другие способы, и что без него можно сделать вполне читаемый вариант :)
Это примерно то, что у нас сейчас (и в статье описано в секции про generic версию, только без 2х классов).
Такой подход имеет место быть (так например работает SuperBuilder в Lombok на сколько знаю), но, к сожалению, он очень тяжело даётся контрибьюторам в проект, особенно рекурсивные конструкции вида <SELF extends MyClass>.
Это, кстати, очень интересная тема. Одно дело — умные книжки про как можно и нельзя, а другое — как потом с таким кодом работать, особенно в OSS, где каждый контрибьютор важен.
Разработчики Testcontainers тоже не глупые и тоже разные книжки читали, но, как и во всём — лучшее враг хорошего :)
Не совсем решаемо — аргументы конструктора не именованны и читать вызовы конструкторов с 10 параметрами — то ещё развлечение
там внизу в комментарии есть problem definition. Ваш "идеальный" код никто не станет использовать, потому что он громоздкий и неудобный (по крайней мере в той библиотеке что я описываю).
Поэтому я и предлагаю переубедить, с примерами кода, иначе ваши комментарии выглядят как "я прочитал книжку, вы всё делаете неправильно, а я — умный".
Пример:
Есть проект — Testcontainers. В нём есть базовый класс GenericContainer.
Есть наследники типа KafkaContainer.
Есть даже промежуточные наследования: MySQLContainer -> JDBCContainer -> GenericContainer.
Должна быть возможность их конфигурировать, чтобы удобно, красиво и вот это вот всё.
Как бы вы решили эту проблему на ровном месте? :)
А можно с примерами Java кода?
А, да, тут абсолютно согласен.
Я там в конце статьи попытался написать "не стоит использовать везде", но такое стоит повторять — этот трюк не везде применим и может сделать больно в неправильных местах (capturing, serialization, classloading, вот это вот всё), особенно в production коде.
А вот для Testcontainers пока что выглядит очень даже норм :) Но всё ещё думаем...