Чистий код та гібридні об’єкти та особливість заздрості


10

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

Гібриди

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

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

Нещодавно я переглядав код на один із своїх робочих об'єктів (це трапляється реалізувати шаблон відвідувачів ) і побачив це:

@Override
public void visit(MarketTrade trade) {
    this.data.handleTrade(trade);
    updateRun(trade);
}

private void updateRun(MarketTrade newTrade) {
    if(this.data.getLastAggressor() != newTrade.getAggressor()) {
        this.data.setRunLength(0);
        this.data.setLastAggressor(newTrade.getAggressor());
    }
    this.data.setRunLength(this.data.getRunLength() + newTrade.getLots());
}

Я одразу сказав собі: "Заздрить особливістю! Ця логіка повинна бути в Dataкласі - конкретно в handleTradeметоді. handleTradeІ завждиupdateRun має відбуватися разом". Але тоді я подумав, що "клас даних - це лише структура даних. Якщо я почніть це робити, то це стане гібридним об'єктом!"public

Що краще і чому? Як ви вирішите, що робити?


2
Чому "дані" взагалі повинні бути структурою даних. Він має чітку поведінку. Тому просто вимкніть всі геттери та сетери, щоб жоден об'єкт не міг маніпулювати внутрішнім станом.
Кормак Малхолл

Відповіді:


9

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

Наприклад, можна стверджувати, що 3D-вектор - це німий запис. Однак це не повинно заважати нам додавати такий метод add, який полегшує додавання векторів. Додавання поведінки не перетворює (не настільки німий) запис у гібрид.

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

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

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

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

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


5

Те, що handleTrade()і updateRun()завжди відбувається разом (а другий метод насправді є відвідувачем і викликає кілька інших методів на об'єкті даних), пахне тимчасовим зв'язком . Це означає, що ви повинні викликати методи у певному порядку, і я б здогадався, що виклик методів, що вийшли з ладу, зламає щось у гіршому випадку або не зможе забезпечити значущий результат у кращому випадку. Не добре.

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

Старий код:

MyObject x = ...;
x.actionOne();
x.actionTwo();
String result = x.actionThree();

Новий код:

MyObject x = ...;
OneResult r1 = x.actionOne();
TwoResult r2 = r1.actionTwo();
String result = r2.actionThree();

Це має ряд переваг:

  • Він переміщує окремі проблеми в окремі об'єкти ( SRP ).
  • Це видаляє тимчасове з'єднання: неможливо викликати методи не в порядку, а підписи методів надають неявну документацію про те, як викликати їх. Ви коли-небудь переглядали документацію, бачили потрібний об’єкт і працювали назад? Я хочу об'єкт Z. Але мені потрібен Y, щоб отримати Z. Щоб отримати Y, мені потрібен X. Ага! У мене є W, який необхідний для отримання X. Ланцюг все це разом, і ваш W тепер можна використовувати для отримання Z.
  • Розщеплення подібних предметів швидше робить їх непорушними, що має численні переваги поза рамками цього питання. Швидкий винос полягає в тому, що незмінні предмети, як правило, призводять до більш безпечного коду.

Немає тимчасової зв'язку між цими двома викликами методів. Поміняйте своїм порядком і поведінка не змінюється.
durron597

1
Спочатку я також думав про послідовне / тимчасове з'єднання, читаючи питання, але потім зауважив, що updateRunметод приватний . Уникнення послідовного з'єднання є гарною порадою, але воно стосується лише дизайну API / публічних інтерфейсів, а не деталей щодо впровадження. Справжнє питання, як видається, чи updateRunповинен бути він у відвідувача чи в класі даних, і я не бачу, як ця відповідь вирішує цю проблему.
амон

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

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

0

З моєї точки зору, клас повинен містити "значення для стану (змінних членів) та реалізації поведінки (функції члена, методи)".

"Невдалі гібридні структури даних" з'являються, якщо ви зробите загальнодоступними змінні учасника класу (або їхні геттери / сеттери), які не повинні бути загальнодоступними.

Тому я не потребую наявності окремих класів для об'єктів даних даних та об'єктів робочих.

Ви повинні мати можливість зберігати змінні стану-члена непублічними (ваш рівень бази даних повинен мати можливість обробляти непублічні змінні учасника)

Особливість заздрості - це клас, який надмірно використовує методи іншого класу. Див. Code_smell . Навчання класу з методами та станом ліквідувало б це.

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