Скільки логіки в Getters


46

Мої колеги говорять мені, що в геттерах і сетерах має бути якомога менше логіки.

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

Приклад того, що я роблю:

public List<Stuff> getStuff()
{
   if (stuff == null || cacheInvalid())
   {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

Приклад того, як робота пропонує мені робити речі (вони цитують «Чистий код» від дядька Боба):

public List<Stuff> getStuff()
{
    return stuff;
}

public void loadStuff()
{
    stuff = getStuffFromDatabase();
}

Наскільки логіка доречна у сеттера / геттера? Яке використання порожніх геттерів та сетерів, крім порушення прихованих даних?


6
Для мене це більше схоже на tryGetStuff () ...
Білл Мішелл

16
Це не «геть». Цей термін використовується для доступу до властивості властивості, а не методу, який ви випадково ввели в назву "отримати".
Борис Янков

6
Я не знаю, чи цей другий приклад є справедливим прикладом цієї чистої кодової книги, яку ви згадуєте, чи хтось отримав неправильний кінець палички про це, але одне, що крихкий безлад - це чистий код.
Джон Ханна

@BorisYankov Ну ... другий метод. public List<Stuff> getStuff() { return stuff; }
Р. Шмітц

Залежно від конкретного випадку використання, я люблю виділяти кешування в окремий клас. Створіть StuffGetterінтерфейс, вкажіть, StuffComputerякий робить обчислення, і загорніть його всередині об'єкта StuffCacher, який відповідає за доступ до кешу або переадресацію дзвінків до того, StuffComputerщо він завершує.
Олександр

Відповіді:


71

Те, як робота каже вам робити речі, кульгавий.

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

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


23
+1 для згадування складності порядку виконання операцій. Як вирішення, можливо, робота попросить мене завжди викликати loadStuff () в конструкторі, але це теж було б погано, оскільки це означає, що його завжди доведеться завантажувати. У першому прикладі дані ліниво завантажуються лише за потреби, що настільки добре, наскільки це може бути.
Лоран

6
Зазвичай я дотримуюсь правила "якщо це дійсно дешево, користуйтеся власником нерухомості. Якщо це дорого, використовуйте функцію". Це, як правило, добре допомагає мені, і називати його правильно, як ви вказали, щоб підкреслити, мені здається, це також добре.
Денис Троллер

3
якщо це може вийти з ладу - це не геттер. У цьому випадку що робити, якщо посилання DB не працює?
Мартін Беккет

6
+1, я трохи в шоці від того, скільки неправильних відповідей було опубліковано. Getters / Setters існують, щоб приховати деталі реалізації, інакше змінна повинна бути оприлюднена.
Ізката

2
Не забувайте, що вимагати виклику loadStuff()функції перед getStuff()функцією також означає, що клас не належним чином обмежує те, що відбувається під кришкою.
rjzii

23

Логіка в геттерах ідеально чудова.

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

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


18

Я думаю, що згідно з "Чистим кодексом" його слід максимально розділити на щось на зразок:

public List<Stuff> getStuff() {
   if (hasStuff()) {
       return stuff;
   }
   loadStuff();
   return stuff;
}

private boolean hasStuff() {
    if (stuff == null) {
       return false;
    }
    if (cacheInvalid()) {
       return false;        
    }
    return true;
} 

private void loadStuff() {
    stuff = getStuffFromDatabase();
}

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

public List<Stuff> getStuff() {
   if (stuff == null || cacheInvalid()) {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

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


8
-1. Справжній головний біль буде тоді, коли абонент застряє, з'ясовуючи, чому простий дзвінок отримувача призвів до повільного доступу до бази даних.
Доменіч

14
@Domenic: Доступ до бази даних у будь-якому випадку повинен бути зроблений, ви не економите нікого, не роблячи цього. Якщо вам це потрібно List<Stuff>, існує лише один спосіб його отримати.
DeadMG

4
@lukas: Дякую, я не пам’ятав усіх хитрощів, використовуваних у коді «Очистити», щоб зробити тривіальні біти коду ще на один рядок довше ;-) Виправлено зараз.
Joonas Pulakka

2
Ви наклепуєте на Роберта Мартіна. Він ніколи не розширить просту булеву диз'юнкцію на функцію дев'яти ліній. Ваша функція hasStuffпротилежна чистому коду.
кевін клайн

2
Я прочитав початок цієї відповіді, і я збирався її обійти, думаючи, "йде інший поклонник книги", і тоді частина "Звичайно, це повна дурниця" потрапила мені в очі. Добре сказано! C -: =
Майк Накіс

8

Вони кажуть мені, що в геттерів та сеттерів повинно бути якомога менше логіки.

Потрібно стільки логіки, скільки необхідно для задоволення потреб класу. Мої особисті переваги - якнайменше, але, зберігаючи код, зазвичай потрібно залишити оригінальний інтерфейс із існуючими геттерами / сеттерами, але вкладати в них багато логіки, щоб виправити новішу бізнес-логіку (як приклад, "клієнти" "геттер у середовищі після 911 повинен відповідати" знати свого замовника "та нормам OFAC у поєднанні з політикою компанії, що забороняє появу клієнтів з певних країн [таких, як Куба чи Іран]).

У вашому прикладі я віддаю перевагу вашому і не люблю зразок "дядько боб", оскільки версія "дядько боб" вимагає від користувачів / обслуговуючого персоналу пам’ятати про те, щоб зателефонувати, loadStuff()перш ніж вони дзвонять getStuff()- це рецепт катастрофи, якщо хтось із ваших технічних працівників забуде (або гірше, ніколи не знав). Більшість місць, де я працював за останнє десятиліття, досі використовують код, якому більше десяти років, тому простота технічного обслуговування є важливим фактором.


6

Ви маєте рацію, ваші колеги помиляються.

Забудьте всі правила про те, що метод отримання або не повинен робити. Клас повинен представляти абстрагування чогось. Ваш клас читається stuff. У Java звичайно використовувати методи 'get' для читання властивостей. Мільярди рядків фреймворків були написані в очікуванні прочитати stuffзателефонувавши getStuff. Якщо ви назвали свою функцію fetchStuffчи щось інше getStuff, то ваш клас буде несумісний із усіма цими рамками.

Ви можете вказати їх на сплячку, де 'getStuff ()' може робити дуже складні речі та викидає RuntimeException при відмові.


Зимова сплячка - це ОРМ, тому пакет сам виражає наміри. Цей намір не так легко зрозуміти, якщо сам пакет не є ОРМ.
FMJaguar

@FMJaguar: це абсолютно легко зрозуміти. Hibernate резюмує операції з базою даних для представлення мережі об'єктів. ОП абстрагує операцію з базою даних для представлення об'єкта, який має властивість з назвою stuff. Обидва приховують деталі, щоб полегшити запис коду дзвінка.
кевін клайн

Якщо цей клас - клас ORM, то наміри виражаються вже в інших контекстах: залишається питання: "Як інший програміст знає побічні ефекти виклику геттера?". Якщо програма містить 1-класові та 10-кілометрові ставки, політика, яка дозволяє виклики бази даних у будь-якому з них, може стати проблемою
FMJaguar

4

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

List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

На відміну від:

myObject.load();
List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

Або ще гірше:

myObject.loadNames();
List<String> names = clientRoster.getNames();
myOjbect.loadEmails();
List<String> emails = clientRoster.getEmails();

Що, як правило, робить інший код набагато вигіднішим і важче читати, тому що вам доведеться почати перебирати всі подібні дзвінки. Крім того, виклик функцій завантажувача або подібного порушує всю мету навіть використання OOP в тому, що ви більше не віддаляєтеся від деталей реалізації об'єкта, з яким працюєте. Якщо у вас є clientRosterоб'єкт, вам не слід дбати про те, як getNamesпрацює, як ви, якщо ви повинні викликати a loadNames, ви просто повинні знати, що getNamesдає вам List<String>імена клієнтів.

Таким чином, звучить, що проблема стосується семантики та найкращої назви функції отримання даних. Якщо компанія (та інші) мають проблеми з префіксом getта setпрефіксом, то як щодо виклику функції щось на зразок retrieveNames? Він говорить про те, що відбувається, але не означає, що операція була б миттєвою, як можна було очікувати від getметоду.

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


2

Виклик геттера повинен проявляти таку ж поведінку, що і читання поля:

  • Вилучення вартості має бути дешевим
  • Якщо встановити значення разом із сеттером, а потім прочитати його за допомогою геттера, значення має бути однаковим
  • Отримання значення не повинно мати побічних ефектів
  • Це не повинно кидати винятку

2
Я не з цим повністю згоден. Я погоджуюся, що він не повинен мати побічних ефектів, але я вважаю, що цілком чудово реалізувати його таким чином, що відрізняє його від поля. Дивлячись на .Net BCL, InvalidOperationException широко використовується при огляді геттерів. Також дивіться відповідь MikeNakis про впорядкування операцій.
Макс

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

1
@TMN: У кращому випадку клас повинен бути організований таким чином, що одержувачам не потрібно виконувати операції, здатні виключати розморожування. Мінімізація місць, які можуть кинути винятки, призводить до менш несподіваних сюрпризів.
hugomg

8
Я буду НЕ згоден з другою точкою з конкретним прикладом: foo.setAngle(361); bar = foo.getAngle(). barце може бути 361, але це може бути законно, 1якщо кути пов'язані з діапазоном.
zzzzBov

1
-1. (1) є дешевим в цьому прикладі - після відкладеної завантаження. (2) в даний час немає «сетер» в прикладі, але якщо хто - то додає один за, і це тільки набори stuffПоглинач буде повертати однакове значення. (3) Ледаче завантаження, як показано в прикладі, не призводить до "видимих" побічних ефектів. (4) є дискусійним, можливо, вагомим пунктом, оскільки введення "ледачого завантаження" згодом може змінити колишній контракт API - але для того, щоб прийняти рішення, потрібно подивитися на цей контракт.
Док Браун

2

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

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

Однак у двох конкретних прикладах, які ви навели, причина, яку я обрав би один над іншою, зовсім інша. Ваш геть ініціалізується при першому доступі, наприклад, Lazy Initialization . Другий приклад вважається ініціалізованим в деякий попередній момент, наприклад, явна ініціалізація .

Коли саме відбувається ініціалізація, може бути, а може і не важливо.

Наприклад, це може бути дуже повільним, і це потрібно робити під час кроку завантаження, коли користувач очікує затримки, а не продуктивність, несподівано ікаючи, коли користувач вперше запускає доступ (наприклад, клацання правою кнопкою користувача, з'являється контекстне меню, користувач вже правою кнопкою миші знову).

Крім того, іноді існує очевидний момент у виконанні, коли відбувається все, що може вплинути / забруднити кешоване значення властивості. Ви навіть можете переконатися, що жодна із залежностей не змінюється і викидає винятки згодом. У цій ситуації має сенс також кешувати значення в цій точці, навіть якщо це не особливо дорого для обчислення, просто щоб уникнути того, щоб зробити виконання коду складнішим і складніше дотримуватися розумово.

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


0

Просто зробіть це так, як сказав @MikeNakis ... Якщо ви просто отримаєте матеріал, то це добре ... Якщо ви робите щось інше, створіть нову функцію, яка виконує цю роботу та зробить її загальнодоступною.

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


1
Будьте уважні з цього приводу, ви можете закінчити виставляти занадто багато свого внутрішнього стану. Ви не хочете , щоб закінчити з великою кількістю порожніх loadFoo()або preloadDummyReferences()або createDefaultValuesForUninitializedFields()методів тільки тому , що початкова реалізація вашого класу , необхідного їм.
TMN

Звичайно ... я просто казав, що якщо ви зробите те, що в назві, стверджується, що проблем не повинно бути багато ... але те, що ви говорите, абсолютно вірно ...
Іван Крояч Карачич

0

Особисто я б викрив вимогу Stuff через параметр у конструкторі, і дозволити тому, який клас інстанціює речі, щоб виконати роботу, з'ясовуючи, звідки він повинен бути. Якщо матеріал недійсний, він повинен повернути null. Я вважаю за краще не намагатися розумних рішень, таких як оригінал OP, тому що це простий спосіб приховати помилки глибоко всередині вашої реалізації, де зовсім не очевидно, що може піти не так, коли щось зламається.


0

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

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

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

    1. Якщо важливо, щоб споживач ніколи не пропускав перевірку кеш-пам'яті, і ви не заперечували проти покарань за продуктивність, тоді підключіть чек усередині геттера і унеможливлюйте, щоб споживач зробив неправильну справу.

    2. Якщо добре пропустити перевірку кешу або дуже важливо, щоб ви гарантували продуктивність O (1) у геттері, тоді використовуйте окремі дзвінки.

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


-1, конструктори повинні будувати, а не ініціалізувати. Якщо розмістити логіку бази даних у конструкторі, цей клас абсолютно не перевіряється, і якщо у вас їх буде більше ніж кілька, час запуску програми стане величезним. І це лише для початку.
Доменіч

@Domenic: Це семантичне та залежне від мови питання. Справа в тому, що об’єкт придатний для використання і забезпечує відповідні інваріанти після, і тільки після цього, він повністю побудований.
хугомг

0

На мою думку, Геттерс не повинен мати в них багато логіки. Вони не повинні мати побічних ефектів, і ви ніколи не повинні отримувати з них виняток. Якщо звичайно, ви знаєте, що робите. Більшість моїх жителів не мають в них логіки і просто йдуть у поле. Але помітний виняток із цього був публічний API, який повинен бути максимально простим у використанні. Тож у мене був один геттер, який вийшов би з ладу, якби не викликали іншого. Рішення? Рядок коду, як var throwaway=MyGetter;у getter, який залежав від нього. Я не пишаюся цим, але все ще не бачу більш чіткого способу зробити це


0

Це схоже на читання з кеша з ледачим завантаженням. Як зазначали інші, перевірка та завантаження можуть належати в інших методах. Завантаження може знадобитися синхронізувати, щоб ви не отримали двадцять ниток, що завантажуються одночасно.

Можливо, буде доречно використовувати ім'я getCachedStuff()для геттера, оскільки воно не матиме послідовного часу виконання.

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


0

Основна логіка, яку я очікував би побачити у людей, що повертають список, - це логіка, щоб переконатися, що список неможливо змінити. Так як обидва вашого прикладу потенційно можуть порушити інкапсуляцію.

щось на зразок:

public List<Stuff> getStuff()
{
    return Collections.unmodifiableList(stuff);
}

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


0

Залежно від конкретного випадку використання, я люблю виділяти кешування в окремий клас. Створіть StuffGetterінтерфейс, вкажіть, StuffComputerякий робить обчислення, і загорніть його всередині об'єкта StuffCacher, який відповідає за доступ до кешу або переадресацію дзвінків до того, StuffComputerщо він завершує.

interface StuffGetter {
     public List<Stuff> getStuff();
}

class StuffComputer implements StuffGetter {
     public List<Stuff> getStuff() {
         getStuffFromDatabase()
     }
}

class StuffCacher implements StuffGetter {
     private stuffComputer; // DI this
     private Cache<List<Stuff>> cache = new Cache<>();

     public List<Stuff> getStuff() {
         if cache.hasStuff() {
             return cache.getStuff();
         }

         List<Stuffs> stuffs = stuffComputer.getStuff();
         cache.store(stuffs);
         return stuffs;
     }
}

Цей дизайн дозволяє легко додавати кешування, видаляти кешування, змінювати основні логіки виведення (наприклад, доступ до БД проти повернення макетних даних) і т. Д. Це трохи зрозуміло, але варто для досить просунутих проектів.


-1

IMHO дуже просто, якщо ви використовуєте дизайн за контрактом. Вирішіть, що повинен надати ваш отримувач, і просто кодуйте відповідно (простий код або якась складна логіка, яка десь може бути залучена або делегована).


+1: Я з вами згоден! Якщо об’єкт призначений просто для зберігання деяких даних, то отримувачі повинні повертати лише поточний вміст об'єкта. У цьому випадку відповідальність за завантаження даних покладається на інший об'єкт. Якщо в контракті сказано, що об'єктом є проксі-сервер запису бази даних, то отримувач повинен отримувати дані на льоту. Це може стати ще складнішим, якщо дані завантажені, але вони не актуальні: чи слід сповіщати об'єкт про зміни в базі даних? Я думаю, що на це питання немає однозначної відповіді.
Джорджо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.