Чому ланцюжок сетерів нетрадиційний?


46

Ведення ланцюга на бобах дуже зручно: не потрібно перевантажувати конструкторів, мегаконструкторів, заводів, а також підвищує читабельність. Я не можу придумати жодних недоліків, якщо ви не хочете, щоб ваш об’єкт був непорушним , і в такому випадку у нього все одно не було б ніяких сетерів. Тож є причина, чому це не конвенція ООП?

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
@Philipp, хоча технічно ви маєте рацію, не сказав би, що new Foo().setBar('bar').setBaz('baz')відчуває себе дуже «вільно». Я маю на увазі, звичайно, це може бути реалізовано точно так само, але я дуже сподіваюся прочитати щось на кшталтFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner

Відповіді:


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);
}

Крім того, пов'язані значення повернення є колосальним болем у справі, коли ви маєте справу зі спадщиною. Дивіться "Цікаво повторюваний шаблон шаблону"

Нарешті, виникає проблема, що конструктор за замовчуванням повинен залишити вас об’єктом, який знаходиться у дійсному стані. Якщо вам потрібно виконати купу команд, щоб відновити об'єкт у дійсному стані, щось вийшло дуже неправильно.


4
Нарешті, справжній катен тут. Спадщина - це реальна проблема. Я думаю, що ланцюжок може бути звичайним сеттером для представлення даних-об'єктів, які використовуються в композиції.
Бен

6
"повернення копії стану з об'єкта" - це не робить.
користувач253751

2
Я вважаю , що сама головна причина для проходження цих рекомендацій (з деякими винятками , як ітератори) є те , що він робить код набагато легше розмірковувати про.
jpmc26

1
@MatthieuM. ніпе. В основному я розмовляю на Java, і там вона переважає в передових API "рідких" - я впевнений, що вона існує і в інших мовах. По суті, ви робите свій тип загальним для типу, що розширює себе. Потім ви додаєте abstract T getSelf()метод із поверненням загального типу. Тепер, замість того, щоб повертатися thisзі своїх сетерів, ви, return getSelf()а потім і будь-який переважаючий клас, просто робите тип загальним у собі та повертаєтесь thisіз getSelf. Таким чином сетери повертають фактичний тип, а не декларуючий тип.
Борис Павук

1
@MatthieuM. справді? Звучить схожий на CRTP для C ++ ...
Марно

33
  1. Збереження кількох натискань клавіш не є переконливим. Це може бути приємно, але конвенції OOP більше дбають про концепції та структури, а не натискання клавіш.

  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
Більшість цих аргументів стосуються будь-якого вільного / можливого інтерфейсу.
Кейсі

@Casey, так. Будівельники (тобто із сеттерами) - це найбільше випадків, коли я бачу прив'язування.
Пол Дрейпер

10
Якщо ви знайомі з ідеєю вільного інтерфейсу, №3 не застосовується, оскільки ви, ймовірно, підозрюєте плавний інтерфейс від типу повернення. Я також повинен не погодитися з "Добре написаним кодом не про приємно оформлений текст". Це не все, але гарно впорядкований текст є досить важливим, оскільки він дозволяє людському читачеві програми зрозуміти його з меншими зусиллями.
Майкл Шоу

1
Ланцюжок налаштування - це не про приємне упорядкування тексту чи про збереження натискань клавіш. Йдеться про зменшення кількості ідентифікаторів. За допомогою ланцюга сетерів ви можете створювати та налаштовувати об’єкт в одному виразі, а це означає, що вам, можливо, не доведеться зберігати його в змінній - змінній, яку ви повинні назвати , і вона залишатиметься там до кінця області застосування.
Ідан Ар'є

3
Щодо пункту №4, це те, що можна вирішити в Java так: public class Foo<T extends Foo> {...}з поверненням сетера Foo<T>. Крім тих методів "сеттер", я вважаю за краще їх називати "методами". Якщо перевантаження працює нормально, то просто Foo<T> with(Bar b) {...}, інакше Foo<T> withBar(Bar b).
YoYo

12

Я не думаю, що це конвенція OOP, вона більше пов'язана з мовним дизайном та його умовами.

Здається, вам подобається використовувати Java. У Java є специфікація JavaBeans, яка вказує тип повернення сеттера, який повинен бути недійсним, тобто він суперечить ланцюгу сетерів. Ця специфікація широко прийнята та впроваджена у різноманітних інструментах.

Звичайно, ви можете запитати, чому не є ланцюжком частини специфікації. Я не знаю відповіді, можливо, ця модель просто не була відома / популярна в той час.


Так, JavaBeans - це саме те, що я мав на увазі.
Бен

Я не думаю, що більшість програм, які використовують JavaBeans, піклуються, але вони можуть (використовуючи відображення, щоб схопити методи, названі "set ...", мають один параметр і недійсні повернення ). Багато програм статичного аналізу скаржаться на те, що не перевіряють значення повернення від методу, який щось повертає, і це може дуже дратувати.

@MichaelT відображаючі дзвінки для отримання методів на Java не вказують тип повернення. Виклик методу таким чином повертається Object, що може призвести до Voidповернення синглів типу , якщо метод вказаний voidяк його тип повернення. В інших новинах, мабуть, "залишення коментаря, але не голосування" є підставою для невдачі аудиту "першого повідомлення", навіть якщо це хороший коментар. Я звинувачую Шога в цьому.

7

Як говорили інші, це часто називають вільним інтерфейсом .

Зазвичай сеттери - це передача викликів змінних у відповідь на логічний код у програмі; ваш клас DTO є прикладом цього. Звичайний код, коли сетери нічого не повертають, це найкраще для цього. Інші відповіді пояснили так.

Однак є кілька випадків, коли вільний інтерфейс може бути хорошим рішенням, вони мають спільне.

  • Константи в основному передаються сетерам
  • Логіка програми не змінює те, що передається сеттерам.

Налаштування конфігурації, наприклад, вільно-нібернат

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");

3
1. Я дійсно не можу погодитися з проблемою читабельності, додаючи екземпляр перед кожним викликом взагалі не робить його зрозумілішим. 2. Шаблони ініціалізації, які ви показали за допомогою js та C #, не мають нічого спільного з конструкторами: те, що ви робили в js, передається одним аргументом, а те, що ви робили в C #, - синтаксичний шугар, який викликає гетерів та сетерів за лаштунками, і java не має шутера-геттера, як у C #.
Бен

1
@Benedictus, я показував різні способи, як мови OOP вирішують цю проблему, не прив'язуючи її. Сенс не полягав у наданні ідентичного коду, суть в тому, щоб показати альтернативи, які роблять ланцюжок непотрібним.
zzzzBov

2
"додавання екземпляра перед кожним викликом взагалі не робить його зрозумілішим". Я ніколи не стверджував, що додавання екземпляра перед кожним викликом робить щось зрозумілішим, я просто сказав, що це порівняно рівнозначно.
zzzzBov

1
VB.NET також має корисне ключове слово "With", яке створює тимчасове посилання компілятора, так що, наприклад, [використання /для представлення розриву рядків] With Foo(1234) / .x = 23 / .y = 47було б еквівалентно Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Такий синтаксис не створює двозначності, оскільки .xсам по собі не може мати іншого значення, крім прив’язки до безпосередньо оточуючого висловлювання "З" [якщо такого немає, або об'єкт не має члена x, то .xбезглуздо]. Oracle не включив нічого подібного в Java, але така конструкція могла б легко вписатися в мову.
supercat

5

Ця техніка фактично використовується в шаблоні Builder.

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

Однак загалом цього уникають, оскільки це неоднозначно. Не очевидно, чи є повернутим об'єктом об'єкт (тому ви можете зателефонувати іншим сеттерам), чи повернутий об'єкт - це значення, яке було щойно призначене (також загальний шаблон). Відповідно, "Принцип найменшого сюрпризу" пропонує вам не намагатися припускати, що користувач хоче бачити те чи інше рішення, якщо це не є основоположним для дизайну об'єкта.


6
Різниця полягає в тому, що під час використання шаблону побудови ви зазвичай пишете об'єкт лише для запису (Builder), який в кінцевому підсумку будує об'єкт, доступний лише для читання (незмінний) (який би клас ви не будували). У зв'язку з цим, мати довгий ланцюжок викликів методів є бажаним, оскільки його можна використовувати як єдиний вираз.
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: create: elements команда автоматично генерує все setters , за замовчуванням .

jQuery свого роду ланцюжок більшості своїх методів дуже схожий спосіб.


Js - гидота
Бен

@Benedictus Я б сказав, що PHP - більша гидота. JavaScript є ідеально чудовою мовою і став досить приємним із включенням функцій ES6 (хоча я впевнений, що деякі люди все ще віддають перевагу CoffeeScript або варіанти; особисто я не прихильник того, як CoffeeScript обробляє змінне масштабування, перевіряючи зовнішню область застосування спочатку, а не обробляти змінні, призначені в локальній області як локальні, якщо явно не вказано як нелокальне / глобальне, як це робить Python).
JAB

@Benedictus Я погоджуюся JAB. У свої коледжні роки я раніше дивився на JS як на гидоту, що хоче бути мовою сценаріїв, але попрацювавши пару років з нею і вивчивши ПРАВИЛЬНИЙ спосіб її використання, я прийшов до ... насправді люблю це . І я думаю, що з ES6 це стане справді зрілою мовою. Але те, що змушує мене повернути голову - це, що моя відповідь стосується насамперед JS? ^^ U
xDaizu

Я фактично працював пару років з js і можу сказати, що я дізнався, що це способи. Тим не менш, я бачу цю мову не що інше, як помилку. Але я згоден, Php набагато гірше.
Бен

@Benedictus Я розумію вашу позицію і поважаю її. Мені все одно подобається. Звичайно, у нього є свої примхи та переваги (яка мова не відповідає?), Але вона неухильно крокує в правильному напрямку. Але ... мені насправді це не так подобається, що мені потрібно це захищати. Я нічого не отримую від людей, які люблять або ненавиджу це ... ха-хаха. Це теж не місце для цього, моя відповідь була взагалі навіть не про JS.
xDaizu
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.