Как стать автором
Обновить

Комментарии 35

Слишком абстрактно и похоже на overengineering. Не помешал бы пример из реальной жизни. Зачем это нужно.
добавил секцию про реальную задачу
Может быть слишком очевидный и с минусами вариант, сорри.
class Parent<T extends Parent> {
	public T fn1(String arg1) {
		// TO DO SMTHNG W/ arg1 HERE ...
		return (T)this;
	}
}
class Descendant<T extends Descendant> extends Parent<Descendant> {
	@Override
	public T fn1(String arg1) {
		return (T)super.fn1(arg1);
	}
	public T fn2(String arg2) {
		// TO DO SMTHNG W/ arg2 HERE ...
		return (T)this;
	}
}

	// ... USING:
	Descendant sc = new Descendant();
	sc.fn1("1").fn2("2");


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

вот без этого паразитного кода

public T fn1(String arg1) {
		return (T)super.fn1(arg1);
	}

Так там копипаста и не нужна если дженерики чуть аккуратнее написать.


class B1<T extends B1<T>> {
    public T fn1(String v) {
        return (T) this;
    }
}

class B2<T extends B2<T>> extends B1<T> {
    public T fn2(String v) {
        return (T) this;
    }
}

class B3<T extends B3<T>> extends B2<T> {
    public T fn3(String v) {
        return (T) this;
    }
}

class FB1 extends B1<FB1> {}
class FB2 extends B2<FB2> {}
class FB3 extends B3<FB3> {}

new FB3().fn1("1").fn2("2").fn3("3")
ну собственно у вас код почти такой же, только там где у меня? у вас T ( у меня RetBuilder)
такая же иерархия классов, только обратное преобразование типов есть, которого лучше избегать по возможности.

я что-то такое делал, но не уверен что с вашими уайлкардами удастся привести FB3 к B1
Какой практический смысл в наследовании билдеров? Это все очень сильно переусложняет, а копипаста решает эту проблему просто и без особых проблем. Еще хотел заметить, что билдер должен на каждый вызов build отдавать новый объект, иначе страдает инкапсуляция.
смысл в том, чтобы передавать дочерний билдер в функции, которые хотят родительский. скрестите билдер и chain of responsibility и там это потребуется.

про создание на каждый builder — зависит от ситуации.
смысл в том, чтобы передавать дочерний билдер в функции, которые хотят родительский


Можно пример, зачем куда-то передавать сам билдер?
И почему не спасло <? extends BaseBuilder> в этом месте?
И почему не спасло <? extends BaseBuilder>
— тогда не получится нормально использовать цепочу. функция установки поля вернет BaseBuilder и на этом все закончится

ну вот упрощенное построение объекта через прогон билдера через цепочку ответственности:
class AddressAttacher implements DtoBuilerChain {
  @override 
  void doChainStep(DtoBuilder builder) {
      bulder.fromAddress(someAddressFrom).toAddress(someAddressTo);
  }
}

DtoBuile builder =  Dto.builder();
listOfDtoBuilerChain.forEach( chain -> chain.doChainStep(builder));
DtoObject obj =  builder.build();
смысл в том, чтобы передавать дочерний билдер в функции, которые хотят родительский. скрестите билдер и chain of responsibility и там это потребуется.

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

Сложно придумать ситуацию, чтобы это не нарушало инкапсуляцию. Честно говоря, не вижу смысла оборачивать dto в еще один объект, если он по сути просто для него обертка с более красивыми методами.
есть объект dto с 40 полями, добавляем новую версию api в которой требуется добавить 1 поле. Можно:
1) копипастить
2) наследоваться
3) инкапсулироваться

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

Не очень понял, вы что, используете слово «инкапсулировать» как эвфемизм для кого-то другого слова? Потому что, судя по контексту, третий пункт обычно называется композицией. И я ничего про неё не говорил. Даже написал, что наследовать dto в целом не противозаконно. А когда я говорил про инкапсуляцию, то имел ввиду, что если после вызова метода build вызвать метод билдера, то он поменяет объект, которым кто-то уже возможно пользуется.
ок

«Честно говоря, не вижу смысла оборачивать dto в еще один объект» если прогонять через цепочку не билдер, а сам объект dto, то получится, что
1) он не иммутабл
2) члены цепочки обмениваются недостроенным объектом. что тоже не очень хорошо
он не иммутабл

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

Что намного лучше, чем неявное изменение объекта третьим объектом.
Параллельная иерархия возникает всегда когда у вас появляются новые представления объекта. И это гораздо лучше чем GodObject, вмещающий в себя все представления, в результате чего при подключении, например, одного DTO появляется зависимость от кучи прикладных библиотек. Конечно, иногда удаются объединения, например, в вариациях mutable/immutable, но и тут не всё может быть гладко. Так что не бойтесь параллельных иерархий — лучше бойтесь GodObject :)
GodObject — это несоблюдение принципа single responsibility — когда у объекта слишком много ответственностей. Так вот у dto вообще никакой ответственности быть не должно, иначе это уже не dto. Его ответственностью в какой-то мере можно назвать хранение данных в своей структуре. Но какой интерфейс для своего создания он предоставит, это к ответственности отношения особо не имеет. Билдер это будет или свои методы, а может он сам себе билдер будет — это нюансы. Откуда же взяться куче прикладных библиотек? От билдера что ли? К тому же, я не говорил, что обязательно нужно всё впихнуть в один класс, я только сказал о том, что наследовать билдеры — плохая на мой взгляд идея.
Я говорю о том что параллельная иерархия возникает часто. Параллельная иерархия может возникать при разделении GodObject. Так что обычно параллельная иерархия не является антипаттерном, подлежащим ликвидации. Также я упомянул, что параллельная иерархия может возникать (хотя мы обычно стремимся избегать этого) при mutable/immutable вариациях объектов, и это как раз относится к обсуждаемой ситуации, так как builder обычно mutable, а объект — нет.
мне не очень понятно, что вы называете иерархией. если это множество наследственно связанных классов, тогда совершенно не ясно, почему «параллельная» и нужда в полиморфном билдере очевидна.

если это просто про набор как-то связанных объектов, то вопрос почему «иерархия»
В чем смысл билдера, если ваш класс и так содержит сеттеры для полей? Билдер имеет смысл для immutable-классов (т.е. когда все поля final) с большим количеством полей, чтобы не вызывать конструктор с тысячей параметров. Если есть сеттеры, почему бы не использовать сам объект вместо билдера?

Но, по-моему, корень вашей проблемы в использовании наследования (которое, из моего скромного опыта, является верным решением менее чем в 1% случаев). Может быть, в вашем случае можно применить композицию вместо наследования?

Касательно приведенного кода: по-моему, вместо хранения ссылки на returnBuilder можно просто использовать (RetBuilder) this, если добавить ораничение RetBuilder extends BuilderImpl и подавить предупреждение об unchecked cast.
В чем смысл билдера, если ваш класс и так содержит сеттеры для полей?
— для наглядности. можно было их и убрать в принципе.

Но, по-моему, корень в
ашей проблемы в использовании наследования (которое, из моего скромного опыта, является верным решением менее чем в 1% случаев). Может быть, в вашем случае можно применить композицию вместо наследования?


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

«по-моему, вместо хранения ссылки на returnBuilder можно просто использовать ». это придется делать в каждом методе, который возвращает билдер — очень некрасиво
если исходные объекты унаследованы

Я как раз про них и говорю: может быть, можно обойтись без наследования исходных объектов?

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

Можно написать вспомогальный метод:
@SuppressWarnings("unchecked")
RetBuilder self() {
     return (RetBuilder) this;
}

RetBuilder foo(int x) {
     nested.setFoo(x);
     return self();
}

Знаю команды, которые используют билдеры в тестах, чтобы повысить читаемость кода. Это хорошо ложится на парадигму Fluent API, которую проповедует AssertJ, например.
Мне кажется довольно сомнительной идея содержать код целого билдера только ради того, чтобы можно было писать
builder.foo(1).bar(2)

вместо
object.setFoo(1);
object.setBar(2);

Да, с билдером не надо писать «object» каждый раз, но сложность кода остается точно такой же. Просто неважная синтаксическая делать.

(Совсем другое дело — использование неизменяемых (immutable) классов. Здесь билдер часто просто необходим, если в классе больше 3-5 полей)
По-хорошему такие конструкции должны быть частью языка. В Groovy/Scala/Kotlin этой хрени уже не надо :)
В scala «эта хрень» всё ещё не поддерживается на уровне языка и реализуется библиотекой. Например, optics от julien-truffaut/Monocle. Но вот языковые свойства скалы, так нелюбимые неосиляторами имплиситы, позволяют реализовывать такую библиотеку действительно лаконично и изящно.
Возможно подразумевается, что при наличии именованных параметров билдеры не нужны.
это, очевидно, неверное утверждение.
Аргументируйте.
Ну вот например, как именованные аргументы помогут в случае наследования?
Я не знаю что вы подразумеваете под «поможет». Приведите пример с билдером — я вам продемонстрирую как то же самое реализовать лаконичнее с именованными параметрами.
по факту после вызова build у нас получается неизменяемый объект.
хотя сама постройка объекта может выполняться в несколько этапов, и внутренний объект меняет свое состояние — на выходе объект неизменяемый.
Да, всего этого можно добиться при использовании именованных параметров. Если вы при этом хотите держать логику вне конструктора, то можете использовать фабричный метод.
Все верно Kotlin с функцией apply очень хорошо подходит для создания объектов, а на Java «правильного полиморфмного билдера» не может быть, все что видел — убоги.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории