Почему цепные сеттеры нетрадиционны?

46

Реализация цепочки на bean-компонентах очень удобна: нет необходимости перегружать конструкторы, мега-конструкторы, фабрики и обеспечивает повышенную читаемость. Я не могу думать о минусах, если только вы не хотите, чтобы ваш объект был неизменным , и в этом случае у него все равно не было бы сеттеров. Так есть ли причина, по которой это не соглашение об ООП?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");
Бен
источник
32
Потому что Java может быть единственным языком, где сеттеры не являются мерзостью для человека ...
Теластин
11
Это плохой стиль, потому что возвращаемое значение не является семантически значимым. Это вводит в заблуждение. Единственным преимуществом является сохранение очень мало нажатий клавиш.
USR
58
Эта модель не является чем-то необычным. Есть даже имя для этого. Это называется свободным интерфейсом .
Филипп
9
Вы также можете абстрагировать это создание в компоновщике для более удобочитаемого результата. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Я бы сделал это, чтобы не противоречить общей идее, что сеттеры - это пустоты.
9
@ Филипп, хотя технически вы правы, не сказал бы, что new Foo().setBar('bar').setBaz('baz')чувствует себя очень "свободно". Я имею в виду, конечно, что это может быть реализовано точно так же, но я бы очень рассчитывал прочитать что-то более похожееFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Уэйн Вернер

Ответы:

50

Так есть ли причина, почему это не соглашение об ООП?

Моя лучшая догадка: потому что это нарушает CQS

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

Например, в C ++ std :: stack :: pop () - это команда, которая возвращает void, а std :: stack :: top () - это запрос, который возвращает ссылку на верхний элемент в стеке. Классически, вы хотели бы объединить два, но вы не можете сделать это и быть в безопасности исключений. (Не проблема в Java, потому что оператор присваивания в Java не генерирует).

Если бы DTO был типом значения, вы могли бы достичь аналогичного конца с

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

Кроме того, цепочка возвращаемых значений - колоссальная боль, когда вы имеете дело с наследованием. Смотрите "Любопытно повторяющийся шаблон"

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

VoiceOfUnreason
источник
4
Наконец, настоящая катаина здесь. Наследование является реальной проблемой. Я думаю, что цепочка может быть обычным установщиком для объектов представления данных, которые используются в композиции.
Бен
6
«возвращение копии состояния из объекта» - он этого не делает.
user253751
2
Я бы сказал, что главная причина для следования этим рекомендациям (за некоторыми заметными исключениями, такими как итераторы) состоит в том, что это значительно упрощает анализ кода .
jpmc26
1
@MatthieuM. Нет. Я в основном говорю на Java, и это распространено там в продвинутых «текучих» API - я уверен, что оно существует и в других языках. По сути, вы делаете свой тип универсальным для типа, расширяющего себя. Затем вы добавляете abstract T getSelf()метод с возвращаемым универсальным типом. Теперь, вместо того чтобы возвращаться thisиз ваших сеттеров, вы, return getSelf()а затем и любой переопределяющий класс просто делает тип универсальным сам по себе и возвращается thisиз getSelf. Таким образом, сеттеры возвращают фактический тип, а не объявленный тип.
Борис Паук
1
@MatthieuM. действительно? Похоже на CRTP для C ++ ...
Бесполезно
33
  1. Сохранение нескольких нажатий клавиш не является обязательным. Это может быть хорошо, но соглашения ООП больше заботятся о концепциях и структурах, а не о нажатиях клавиш.

  2. Возвращаемое значение не имеет смысла.

  3. Даже более чем бессмысленно, возвращаемое значение вводит в заблуждение, поскольку пользователи могут ожидать, что возвращаемое значение будет иметь смысл. Они могут ожидать, что это «неизменный сеттер»

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }

    На самом деле ваш сеттер мутирует объект.

  4. Это не работает с наследованием.

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 

    я могу написать

    new BarHolder().setBar(2).setFoo(1)

    но нет

    new BarHolder().setFoo(1).setBar(2)

Для меня, № 1 до № 3 являются важными. Хорошо написанный код не о приятном расположении текста. Хорошо написанный код о фундаментальных понятиях, отношениях и структуре. Текст - это только внешнее отражение истинного значения кода.

Пол Дрэйпер
источник
2
Однако большинство этих аргументов применимы к любому свободному / цепочечному интерфейсу.
Кейси
@ Кейси, да. Строители (то есть с сеттерами) - это наиболее вероятный случай цепочки.
Пол Дрейпер
10
Если вы знакомы с идеей свободного интерфейса, № 3 не применяется, так как вы, вероятно, будете подозревать свободный интерфейс из возвращаемого типа. Я также должен не согласиться с «Хорошо написанный код не о приятно организованном тексте». Это еще не все, но хорошо организованный текст довольно важен, поскольку позволяет читателю программы понять его с меньшими усилиями.
Майкл Шоу
1
Цепочка сеттера - это не приятное расположение текста и не сохранение нажатий клавиш. Речь идет о сокращении количества идентификаторов. С помощью цепочки сеттера вы можете создавать и настраивать объект в одном выражении, что означает, что вам, возможно, не придется сохранять его в переменной - переменной, которую вы должны будете назвать , и она останется там до конца области действия.
Идан Арье
3
Что касается пункта # 4, это то, что может быть решено в Java следующим образом: public class Foo<T extends Foo> {...}с возвращением сеттера Foo<T>. Кроме того, эти методы «setter», я предпочитаю вызывать их «with» методами. Если перегрузка работает нормально, то просто Foo<T> with(Bar b) {...}, иначе Foo<T> withBar(Bar b).
YoYo
12

Я не думаю, что это соглашение ООП, оно больше связано с дизайном языка и его соглашениями.

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

Конечно, вы можете спросить, почему это не является частью спецификации. Я не знаю ответа, возможно, этот шаблон просто не был известен / популярен в то время.

QBD
источник
Да, JavaBeans - это именно то, что я имел в виду.
Бен
Я не думаю, что большинство приложений, использующих JavaBeans, заботятся об этом, но они могут (используя рефлексию, чтобы захватить методы с именем 'set ...', имеющие один параметр и возвращающие void ). Многие программы статического анализа жалуются на то, что не проверяют возвращаемое значение из метода, который возвращает что-то, и это может быть очень раздражающим.
Светоотражающие вызовы @MichaelT для получения методов в Java не указывают тип возвращаемого значения. Вызов метода, возвращаемого этим способом Object, может привести к Voidвозвращению синглтона типа , если метод указывает в voidкачестве своего возвращаемого типа. В других новостях, по всей видимости, «оставление комментария, но не голосование» является основанием для провала аудита «первой публикации», даже если это хороший комментарий. Я обвиняю шога в этом.
7

Как говорили другие люди, это часто называют свободным интерфейсом .

Обычно сеттеры - это передача вызовов в переменных в ответ на логический код в приложении; ваш класс DTO является примером этого. Обычный код, когда сеттеры ничего не возвращают, является нормальным для этого. Другие ответы объяснили путь.

Однако есть несколько случаев, когда свободный интерфейс может быть хорошим решением, у них есть общее.

  • Константы в основном передаются сеттерам
  • Программная логика не меняет то, что передается сеттерам.

Настройка конфигурации, например, fluent-nhibernate

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

Настройка тестовых данных в модульных тестах с использованием специальных TestDataBulderClasses (Object Mothers)

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

Однако создать хороший свободный интерфейс очень сложно , поэтому оно того стоит, когда у вас много «статических» настроек. Также свободный интерфейс не должен смешиваться с «нормальными» классами; следовательно, шаблон строителя часто используется.

Ян
источник
5

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

Вместо:

DTO dto = new DTO().setFoo("foo").setBar("bar");

Можно написать:

(в JS)

var dto = new DTO({foo: "foo", bar: "bar"});

(в C #)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(на Java)

DTO dto = new DTO("foo", "bar");

setFooи setBarтогда больше не нужны для инициализации и могут быть использованы для мутации позже.

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

Например

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

затрудняет чтение и понимание происходящего. Переформатирование в:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

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

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");
zzzzBov
источник
3
1. Я действительно не могу согласиться с вопросом о читаемости, добавление экземпляра перед каждым вызовом не делает его более понятным. 2. Шаблоны инициализации, которые вы показали с js и C #, не имеют ничего общего с конструкторами: то, что вы делали в js, передается в один аргумент, а то, что вы делали в C #, это синтаксический шугар, который вызывает геттеры и сеттеры за кулисами, и java не имеет геттер-сеттер, как в C #.
Бен
1
@Benedictus, я показывал различные способы, которыми языки ООП решают эту проблему без цепочки. Суть не в том, чтобы предоставить идентичный код, а в том, чтобы показать альтернативы, которые делают ненужным создание цепочки.
zzzzBov
2
«добавление экземпляра перед каждым вызовом не делает его более понятным» Я никогда не утверждал, что добавление экземпляра перед каждым вызовом делает что-то более понятным, я просто говорил, что это было относительно эквивалентно.
zzzzBov
1
VB.NET также имеет пригодное для использования ключевое слово «With», которое создает временную ссылку компилятора, так что, например, [using /to представляет разрыв строки] With Foo(1234) / .x = 23 / .y = 47будет эквивалентно Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Такой синтаксис не создает двусмысленности, так .xкак сам по себе не может иметь никакого значения, кроме как связываться с непосредственно окружающим оператором «С» [если его нет, или у объекта нет члена x, то не .xимеет смысла]. Oracle не включил ничего подобного в Java, но такая конструкция легко вписалась бы в язык.
суперкат
5

Эта техника фактически используется в паттерне Builder.

x = ObjectBuilder()
        .foo(5)
        .bar(6);

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

Корт Аммон
источник
6
Разница в том, что при использовании шаблона построителя вы обычно пишете объект только для записи (построитель), который в конечном итоге создает объект только для чтения (неизменяемый) (независимо от класса, который вы создаете). В связи с этим желательно иметь длинную цепочку вызовов методов, поскольку ее можно использовать как одно выражение.
Darkhogg
«или если возвращаемый объект - это значение, которое было только что назначено», я ненавижу это, если я хочу сохранить только что переданное значение, я сначала помещу его в переменную. Может быть полезно получить значение, которое было ранее назначено, хотя (я полагаю, некоторые контейнерные интерфейсы делают это).
JAB
@JAB Я согласен, я не такой фанат этой нотации, но она имеет свои места. То, что приходит на ум, это Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); то, что я думаю, что оно также приобрело популярность, потому что оно отражает a = b = c = d, хотя я не уверен, что популярность была обоснованной. Я видел упомянутое вами возвращение предыдущего значения в некоторых элементарных библиотеках операций. Мораль истории? Это даже более запутанно, чем я это озвучил =)
Cort Ammon
Я полагаю, что академики становятся немного педантичными: в этой идиоме нет ничего неправильного. Это чрезвычайно полезно для добавления ясности в некоторых случаях. Возьмем, к примеру, следующий класс форматирования текста, который выравнивает каждую строку строки и рисует вокруг нее рамку ascii-art new BetterText(string).indent(4).box().print();. В этом случае я обошел кучу гоблидов и взял строку, отступил, поместил ее в коробку и вывел ее. Возможно, вы даже захотите метод дублирования внутри каскада (например, скажем, .copy()), чтобы все последующие действия больше не изменяли оригинал.
tgm1024
2

Это больше комментарий, чем ответ, но я не могу комментировать, так что ...

Я просто хотел упомянуть, что этот вопрос удивил меня, потому что я не вижу в этом ничего необычного . На самом деле, в моей среде работы (веб-разработчик) очень и очень часто.

Например, вот как доктрина Symfony: генерирует: entity команда автоматически генерирует все setters , по умолчанию .

JQuery объединяет большинство своих методов очень похожим образом.

xDaizu
источник
Js это мерзость
Бен
@Benedictus Я бы сказал, что PHP - большая мерзость. JavaScript - прекрасный язык, и он стал довольно приятным с включением функций ES6 (хотя я уверен, что некоторые люди все еще предпочитают CoffeeScript или варианты; лично я не фанат того, как CoffeeScript обрабатывает область видимости переменных, так как он проверяет внешнюю область видимости сначала вместо того, чтобы рассматривать переменные, назначенные в локальной области видимости, как локальные, если это явно не указано как нелокальное / глобальное, как это делает Python).
JAB
@Benedictus Я согласен с JAB. В студенческие годы я привык смотреть на JS как на ненормальный язык сценариев, но я, работая пару лет с ним и выучив ПРАВИЛЬНЫЙ способ его использования, пришел к ... на самом деле, полюбил его , И я думаю, что с ES6 он станет действительно зрелым языком. Но что заставляет меня повернуть голову, так это то, что мой ответ имеет отношение к JS? ^^ U
xDaizu
Я действительно работал пару лет с JS и могу сказать, что я узнал, что это способы. Тем не менее я вижу этот язык как не более чем ошибку. Но я согласен, Php намного хуже.
Бен
@Benedictus Я понимаю вашу позицию и уважаю ее. Мне все еще нравится это все же. Конечно, у него есть свои причуды и льготы (а какой язык нет?), Но он неуклонно движется в правильном направлении. Но ... на самом деле мне это не нравится так сильно, что мне нужно защищать это. Я ничего не получаю от людей, которые любят или ненавидят это ... хахаха Также, это не место для этого, мой ответ вообще не касался JS.
xDaizu