Чи повинен геттер кидати виняток, якщо його об’єкт має недійсний стан?


55

Я часто стикаюся з цією проблемою, особливо на Java, навіть якщо вважаю, що це загальна проблема OOP. Тобто: підвищення винятку виявляє проблему дизайну.

Припустимо, у мене є клас, який має String nameполе і String surnameполе.

Потім він використовує ці поля для складання повного імені людини для того, щоб відобразити його в якомусь документі, наприклад, рахунку-фактурі.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Тепер я хочу посилити поведінку свого класу, видавши помилку, якщо displayCompleteNameOnInvoiceвиклик до того, як ім'я було призначено. Здається, це гарна ідея, чи не так?

Я можу додати getCompleteNameметод підвищення винятків до методу. Але таким чином я порушую «неявний» контракт із користувачем класу; в цілому одержувачі не повинні викидати винятки, якщо їх значення не встановлено. Гаразд, це не стандартний геттер, оскільки він не повертає жодного поля, але, з точки зору користувача, відмінність може бути надто тонкою, щоб думати про це.

Або я можу викинути виняток ізсередини displayCompleteNameOnInvoice. Але для цього я повинен перевірити безпосередньо nameабо surnameполя і при цьому я б порушити абстракції , представлені getCompleteName. Саме цей метод відповідає за перевірку та створення повного імені. Він навіть міг би вирішити, грунтуючись на рішенні на інших даних, що в деяких випадках це достатньо surname.

Таким чином, видається, що єдиною можливістю є зміна семантики методу getCompleteNameна composeCompleteName, що передбачає більш "активну" поведінку і, відповідно, здатність кидати виняток.

Це краще рішення дизайну? Я завжди шукаю найкращий баланс між простотою та правильністю. Чи є проектна посилання на це питання?


8
"в цілому одержувачі не повинні викидати винятки, якщо їх значення не встановлено." - Що вони повинні робити тоді?
Ротем

2
@scriptin Тоді, керуючись цією логікою, displayCompleteNameOnInvoiceможна просто кинути виняток, якщо getCompleteNameповертається null, чи не так?
Ротем

2
@Rotem це може. Питання в тому, чи варто.
сценарій

22
З цього приводу програмісти фальшивості вірять у імена - це дуже добре, чому "ім'я" та "прізвище" є дуже вузькими поняттями.
Ларс Віклунд

9
Якщо ваш об’єкт може мати недійсний стан, ви вже накрутили.
користувач253751

Відповіді:


151

Не дозволяйте будувати свій клас без призначення імені.


9
Це цікаве, але досить радикальне рішення. Багато разів ваші заняття мають бути більш текучими, що дозволяє станам, які стають проблемою лише в тому випадку, якщо і коли будуть викликані певні методи, в іншому випадку ви отримаєте проліферативну кількість невеликих класів, для яких потрібно використовувати занадто багато пояснень.
AgostinoX

71
Це не коментар. Якщо він застосує поради у відповіді, у нього більше не буде описана проблема. Це відповідь.
DeadMG

14
@AgostinoX: Ви повинні робити ваші заняття рідкими лише в разі крайньої необхідності. В іншому випадку вони повинні бути максимально інваріантними.
DeadMG

25
@gnat: ви чудово працюєте тут, ставлячи під сумнів якість кожного запитання та кожної відповіді на PSE, але іноді ви IMHO трохи надто охайний.
Док Браун

9
Якщо вони можуть бути дійсно факультативними, у нього немає причин кидати його в першу чергу.
DeadMG

75

Відповідь, яку надає DeadMG, значною мірою зазначає це, але дозвольте мені викласти це трохи інакше: уникайте об'єктів з недійсним станом. Об'єкт повинен бути "цілим" у контексті завдання, яке він виконує. Або не повинно існувати.

Тож якщо ви хочете плинність, використовуйте щось на зразок Builder Pattern . Або що-небудь інше, що є окремою переробкою об'єкта в будівництві на відміну від "реальної речі", коли останній гарантовано має дійсний стан і є тим, що фактично піддається операціям, визначеним у цьому стані (наприклад getCompleteName).


7
So if you want fluidity, use something like the builder pattern- плинність або незмінність (якщо тільки це якимось чином ви не маєте на увазі). Якщо потрібно створити незмінні об'єкти, але потрібно відкласти встановлення деяких значень, то наступним шаблоном є встановлення їх по одному на об’єкті будівельника та створення непорушного лише після того, як ми зібрали всі значення, необхідні для правильної держава ...
Конрад Моравський

1
@KonradMorawski: Я розгублений. Ви уточнюєте чи суперечать моїй заяві? ;)
back2dos

3
Я намагався доповнити його ...
Конрад Моравський

40

Не має об’єкта з недійсним станом.

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

Отже, питання до цієї частини - "чому ви дозволяєте ім'я чи прізвище бути недійсними?" Якщо це не те, що є дійсним у класі, щоб він працював належним чином, не дозволяйте. Запропонуйте конструктору чи конструктору, який належним чином підтверджує створення об'єкта, і якщо це неправильно, поставте питання в цьому пункті. Ви також можете скористатись одним із наявних @NotNullприміток , щоб допомогти повідомити, що "це не може бути нульовим" та застосувати його в кодуванні та аналізі.

За наявності цієї гарантії стає набагато простіше міркувати про код, що він робить, і не потрібно викидати винятки в непарних місцях або ставити зайві перевірки навколо функцій геттера.

Геттери, які роблять більше.

На цю тему є зовсім небагато. Ви отримали скільки логіки в Getters та що слід дозволити всередині геттерів та сетерів? звідси і геттери та сетери, що виконують додаткову логіку на Stack Overflow. Це питання, яке виникає знову і знову в дизайні класів.

Основою цього є:

в цілому одержувачі не повинні викидати винятки, якщо їх значення не встановлено

І ти маєш рацію. Вони не повинні. Вони повинні виглядати «тупими» до решти світу і робити те, що очікується. Занадто багато логіки там призводить до питань, де порушується закон найменшого здивування. І давайте зіткнутися з цим, ви насправді не хочете загортати гетерів, try catchтому що ми знаємо, як сильно ми любимо це робити взагалі.

Існують також ситуації, коли ви повинні використовувати такий getFooметод, як фреймворк JavaBeans, і коли у вас є щось, що викликає EL, очікуючи отримати бін (так що <% bar.foo %>насправді дзвінки getFoo()в класі - відміняючи «якщо боб буде робити композицію або повинен що залишається для перегляду? ", тому що можна легко придумати ситуації, коли те чи інше може бути правильною відповіддю)

Зрозумійте також, що для даного геттера можна вказати в інтерфейсі або бути частиною раніше відкритого загальнодоступного API для класу, який отримує відновлення (у попередній версії просто було повернуто "fullName", і рефакторинг зламався це на два поля).

В кінці дня...

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

Намагаючись дотриматись смислової чистоти істоти private T foo; T getFoo() { return foo; }, що потрапляє, у якийсь момент ви увійдете в проблеми. Іноді код просто не відповідає тій моделі, а контури, що проходять, намагаючись утримати це, просто не мають сенсу ... і в кінцевому підсумку це ускладнює проектування.

Приймайте іноді, що ідеали дизайну не можуть бути реалізовані так, як ви хочете їх у коді. Вказівки - це вказівки - не кайдани.


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

2
Можливість міркувати про код - це ключове значення для того, щоб писати код за допомогою API чи вміти підтримувати код. Якщо клас має конструктор, який дозволяє йому знаходитись у недійсному стані, це набагато важче продумати "добре, що це робить?" тому що зараз вам доведеться розглянути більше ситуацій, ніж дизайнер класу розглядав чи задумав. Це додаткове концептуальне навантаження приходить до штрафу більше помилок і більш тривалого часу для написання коду. З іншого боку, якщо ви можете подивитися на код і сказати "ім'я та прізвище ніколи не буде нульовим", писати код, використовуючи їх, стає набагато простіше.

2
Неповне не обов'язково означає недійсне. Рахунок без квитанції може бути розроблений, і публічний API, який він подає, відображатиме, що таке дійсне стан. Якщо surnameабо nameє нульовим - дійсний стан для об'єкта, тоді обробляйте його відповідно. Але якщо це не є дійсним станом - змушує методи викидати винятки в межах класу, йому слід запобігати перебуванню в цьому стані з моменту його ініціалізації. Ви можете обходити неповні об'єкти, але пройти навколо недійсних об'єктів проблематично. Якщо нульове прізвище виняткове, спробуйте запобігти його раніше, ніж пізніше.

2
Пов’язана публікація в блозі для читання: Проектування ініціалізації об'єктів та відзначте чотири підходи до ініціалізації змінної екземпляра. Один з них - дозволити йому перебувати в недійсному стані, хоча зауважте, що це є винятком. Якщо ви намагаєтеся цього уникнути, такий підхід не є дійсним, і вам потрібно буде працювати з іншими підходами - які передбачають забезпечення дійсного об'єкта в усі часи. З блогу: "Об'єкти, які можуть мати недійсні стани, важче використовувати і важче зрозуміти, ніж ті, які завжди дійсні". і це ключово.

5
"Робіть те, про що найпростіше міркувати". Це
Бен

5

Я не хлопець на Java, але це, здається, дотримується обох обмежень, які ви представили.

getCompleteNameне кидає виняток, якщо імена неініціалізовані, і displayCompleteNameOnInvoiceтак.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}

Щоб розширити, чому це правильне рішення: Таким чином, абоненту getCompleteName()не потрібно дбати про те, чи властивість насправді зберігається або чи обчислюється під час руху.
Барт ван Інген Шенау

18
Щоб розширити питання про те, чому це рішення є божевільним, вам доведеться перевіряти наявність недійсності при кожному виклику getCompleteName, який є огидним.
DeadMG

Ні, @DeadMG, ви повинні перевірити це лише у тих випадках, коли викид повинен бути викинутий, якщо getCompleteName поверне нуль. Що саме так і має бути. Це рішення правильне.
Dawood ibn Kareem

1
@DeadMG Так, але ми не знаємо, якими є ІНШІ вживання getCompleteName()в решті класу чи навіть в інших частинах програми. У деяких випадках повернення nullможе бути саме тим, що потрібно. Але хоча існує ОДИН метод, специфіка якого "кинути виняток у цьому випадку", це ТОЧНО те, що ми повинні кодувати.
Dawood ibn Kareem

4
Можуть бути ситуації, коли це найкраще рішення, але я не можу уявити жодного. У більшості випадків це здається надто складним: один метод кидає з неповними даними, інший повертає нуль. Боюся, це, ймовірно, буде неправильно використане абонентами.
sleske

4

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

Відповідь:

Так, слід.

Простий приклад: FileStream.Lengthу C # /. NET , який викидає, NotSupportedExceptionякщо файл не шукається.


Коментарі не для розширеного обговорення; ця розмова перенесена в чат .
maple_shaft

1

У вас є вся справа перевірки імені догори дном.

getCompleteName()не повинен знати, які функції будуть використовувати його. Таким чином, відсутність nameдзвінка displayCompleteNameOnInvoice()не є getCompleteName()відповідальністю.

Але, nameце чітка передумова displayCompleteNameOnInvoice(). Таким чином, він повинен взяти на себе відповідальність за перевірку стану (Або він повинен делегувати відповідальність перевірки на інший метод, якщо ви хочете).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.