Краще мати 2 методи з чітким значенням, або просто 1 метод подвійного використання?


30

Щоб спростити інтерфейс, краще просто не мати getBalance()методу? Перехід 0до charge(float c);волі дасть той самий результат:

public class Client {
    private float bal;
    float getBalance() { return bal; }
    float charge(float c) {
        bal -= c;
        return bal;
    }
}

Можливо, запишіть у javadoc? Або просто залиште це користувачеві класу, щоб зрозуміти, як отримати баланс?


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

1
@corsiKa нах. Це лише приклад. Але, так, я б використав float, щоб представляти гроші в реальному класі ... Дякую, що нагадав мені про небезпеку чисел з плаваючою комою.
Девід

10
Деякі мови розрізняють мутатори та аксесуари для гарного ефекту. Було б жахливо дратувати неможливість отримати баланс екземпляра, доступного лише як константа!
JDługosz

Відповіді:


90

Ви, здається, припускаєте, що складність інтерфейсу вимірюється кількістю елементів, які він має (методи, в даному випадку). Багато хто стверджує, що, якщо пам'ятати, що chargeметод може бути використаний для повернення балансу, Clientдодає набагато більшу складність, ніж наявність додаткового елемента getBalanceметоду. Зробити речі більш явними набагато простіше, особливо до того, що це не залишає двозначності, незалежно від більшої кількості елементів в інтерфейсі.

Крім того, виклик charge(0)порушує принцип найменшого здивування , відомий також як показник WTF за хвилину (з чистого коду, зображення нижче), що ускладнює новим членам команди (або поточним, через деякий час далеко від коду) до вони розуміють, що дзвінок насправді використовується для отримання балансу. Подумайте, як реагували б інші читачі:

введіть тут опис зображення

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

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

public class Client {
  private float bal;
  float getBalance() { return bal; }
  void charge(float c) { bal -= c; }
}

25
Суть про розділення команд-запитів надзвичайно важлива, IMO. Уявіть, що ви новий розробник цього проекту. Переглядаючи код, відразу зрозуміло, що getBalance()повертає якесь значення і нічого не змінює. З іншого боку, charge(0)схоже, що це, ймовірно, щось змінює ... можливо, він надсилає подію PropertyChanged? І що це повернення, попереднє значення чи нове значення? Тепер ви повинні шукати це в документах і витрачати мозкові сили, чого можна було б уникнути, використовуючи більш чіткий метод.
Mage Xy

19
Крім того, використовуючи charge(0)як спосіб отримання балансу, оскільки це, мабуть, не має ніякого ефекту, тепер привертає вас до цієї деталі реалізації; Я легко уявляю собі майбутню вимогу, коли відстеження кількості зарядів стає актуальним, і в цей момент заповнення бази даних кодами зарядів, які насправді не є звинуваченнями, стає больовим моментом. Ви повинні надати елементи інтерфейсу, які означають те, що потрібно робити вашим клієнтам, а не ті, які просто трапляються для того, що потрібно робити вашим клієнтам.
Бен

12
Застереження CQS необов'язково підходить. Для charge()методу, якщо цей метод є атомарним в паралельній системі, може бути доцільним , щоб повернути нове або старе значення.
Дітріх Епп

3
Я визнав ваші два цитати щодо CQRS надзвичайно непереконливими. Наприклад, мені взагалі подобаються ідеї Мейєра, але дивлячись на тип повернення функції, щоб сказати, чи вона щось мутує, є довільною та ненапруженою. Для багатьох одночасних або незмінних структур даних потрібні мутація + повернення. Як би ви додали до рядка, не порушуючи CQRS? Чи є якісь кращі цитати?
user949300

6
Майже жоден код не відповідає розділу запитів команд. Це не є ідіоматичним у будь-яких популярних об'єктно-орієнтованих мовах і порушується вбудованими API кожного мови, який я коли-небудь використовував. Охарактеризувати це як "кращу практику", таким чином, здається дивним; чи можете ви назвати навіть один програмний проект, який насправді відповідає цій схемі?
Марк Амері

28

ІМО, замінивши getBalance()з charge(0)через додаток не є спрощення. Так, це менше рядків, але це затьмарює значення charge()методу, який потенційно може викликати головні болі внизу лінії, коли вам чи комусь іншому потрібно переглянути цей код.

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


13

Важливо пам’ятати, що ваш код повинен бути самодокументованим. Коли я дзвоню charge(x), я розраховую xстягнути плату. Інформація про баланс є вторинною. Більше того, я, можливо, не знаю, як charge()реалізується, коли я його закликаю, і я точно не знаю, як це реалізується завтра. Наприклад, розгляньте це потенційне майбутнє оновлення для charge():

float charge(float c) {
    lockDownUserAccountUntilChargeClears();
    bal -= c;
    Unlock();
    return bal;
}

Раптом використання charge()балансу не виглядає так добре.


Так! Хороший момент, який chargeнабагато більш важкий.
Дедуплікатор

2

Використання charge(0);для отримання балансу є поганою ідеєю: одного дня хтось може додати там якийсь код для реєстрації зарядів, що проводяться, не усвідомлюючи іншого використання функції, і тоді кожен раз, коли хтось отримає баланс, він буде реєструватися як плата. (Існують способи навколо цього, наприклад, умовне твердження, яке говорить щось на зразок:

if (c > 0) {
    // make and log charge
}
return bal;

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

Коротше кажучи: не покладайтеся ні на своїх користувачів, ні на своїх наступників програмістів, розуміючи, що charge(0);це правильний спосіб отримати баланс, адже, якщо немає документації, яку вони гарантовано не пропустять, відверто кажучи, це виглядає як найстрашніший спосіб отримання балансу. можливо.


0

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


-2

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

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

(Це модель дизайну, яку я не пригадую конкретної назви: визначення складного інтерфейсу, що викликає виклик, з точки зору мінімально зручного втілення в реалізацію. У Classic Mac OS мінімальний інтерфейс для операцій малювання називався "вузьким місцем" але це, мабуть, не є популярним терміном.)

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


1
Дійсно, у мене були випадки, коли для полегшення повного тестування покриття було обрано більш мінімальний інтерфейс. Але це не обов'язково піддається користувачеві класу. Наприклад, вставлення , додавання та видалення можуть бути простими обгортками загальної заміни, у яких всі шляхи управління добре вивчені та перевірені.
JDługosz

Я бачив такий API. Використовує його розподільник Lua 5.1+. Ви надаєте одну функцію, і виходячи з параметрів, які вона передає, ви вирішуєте, чи намагається вона виділити нову пам’ять, перемістити пам'ять чи перерозподілити пам'ять зі старої пам'яті. Це болісно , щоб написати такі функції. Я набагато скоріше, щоб Луа щойно дав вам дві функції: одну для розподілу та одну для розселення.
Нікол Болас

@NicolBolas: І ніхто для перерозподілу? У будь-якому випадку, цей додаток має "перевагу" майже таким самим, як reallocіноді, для деяких систем.
Дедуплікатор

@Deduplicator: розподіл проти перерозподілу ... ОК. Не дуже здорово ставити їх на ту саму функцію, але це не так вже й погано, як розміщувати розсилку в тій же. Це також легко розрізнити.
Нікол Болас

Я б сказав, що конфліктування взаємозв'язку з (пере) розподілом є особливо поганим прикладом цього типу інтерфейсу. (Передача має зовсім інший контракт: він не призводить до дійсної вказівки.)
Кевін Рейд,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.