Коли перерахунки НЕ кодовим запахом?


17

Дилема

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

Як такий, я шукаю вказівки та / або випадки використання, коли перерахунки НЕ є запахом коду, а насправді є дійсною конструкцією.

Джерела:

"ПОПЕРЕДЖЕННЯ Як правило, перерахунки є кодовими запахами і повинні бути відновлені до поліморфних класів. [8]" Seemann, Mark, Dependency Injection in .Net, 2011, p. 342

[8] Мартін Фаулер та ін., Рефакторинг: Покращення дизайну існуючого коду (Нью-Йорк: Аддісон-Веслі, 1999), 82.

Контекст

Причина моєї дилеми - торговий API. Вони передають мені потік даних Tick, надсилаючи за допомогою цього методу:

void TickPrice(TickType tickType, double value)

де enum TickType { BuyPrice, BuyQuantity, LastPrice, LastQuantity, ... }

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

Dictionary<TickType,double> LastValues

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

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

Перегляд точки зору

Деякі рішення та дії базуються на цих, TickTypeі я не можу придумати спосіб усунення тверджень enum / switch. Найчистішим рішенням, про який я можу придумати, є використання фабрики та повернення реалізації на основі TickType. Навіть тоді у мене все ще буде оператор перемикання, який повертає реалізацію інтерфейсу.

Нижче перелічено один із зразкових класів, де я маю сумніви, що я можу неправильно використовувати переписку:

public class ExecutionSimulator
{
  Dictionary<TickType, double> LastReceived;
  void ProcessTick(TickType tickType, double value)
  {
    //Store Last Received TickType value
    LastReceived[tickType] = value;

    //Perform Order matching only on specific TickTypes
    switch(tickType)
    {
      case BidPrice:
      case BidSize:
        MatchSellOrders();
        break;
      case AskPrice:
      case AskSize:
        MatchBuyOrders();
        break;
    }       
  }
}

26
Я ніколи не чув, щоб переліки були кодовим запахом. Чи можете ви включити посилання? Я думаю, що вони мають величезний сенс для обмеженої кількості потенційних цінностей
Річард Тінгл

25
У яких книгах кажуть, що енуми - це кодовий запах? Отримайте кращі книги.
ЖакБ

3
Тож відповідь на питання була б "більшість часу".
gnasher729

5
Приємне, безпечне значення, яке передає значення? Що гарантує, що належні ключі використовуються у словнику? Коли це кодовий запах?

7
Без контексту, я думаю, кращою формулюванням було бenums as switch statements might be a code smell ...
заклик

Відповіді:


31

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

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


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

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

@Jules - "Якщо ви впевнені, що новий тип галочок ніколи не буде доданий ..." Це твердження неправильне на стільки рівнях. Клас для кожного окремого типу, незважаючи на те, що кожен тип має таку саму поведінку, є далеко не "розумним" підходом, який можна отримати. Це не до того, як у вас буде інша поведінка і не почнете потребувати операторів "переключити / якщо-тоді", де потрібно намалювати лінію. Це, звичайно, не ґрунтується на тому, чи можу я в майбутньому додати більше значень перерахунків.
Данк

@dunk Я думаю, ти неправильно зрозумів мій коментар. Я ніколи не пропонував створювати кілька типів з однаковою поведінкою - це було б шалено.
Жуль

1
Навіть якщо ви "перерахували всі можливі значення", це не означає, що тип ніколи не матиме додаткової функціональності на примірник. Навіть щось на зразок Місяця може мати і інші корисні властивості, наприклад кількість днів. Використання enum негайно запобігає поширенню концепції, яку ви моделюєте. Це в основному механізм / модель анти-ООП.
Дейв Кузен

17

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

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

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


Ви хочете сказати, що коли enums містить іменники як записи (наприклад, Colors), вони, ймовірно, використовуються правильно. А коли вони є дієсловами (З’єднані, Візуалізація), то це знак того, що його неправильно використовують?
stromms

2
@stromms, граматика - це жахливий посібник до архітектури програмного забезпечення. Якби TickType був інтерфейсом замість enum, якими були б методи? Я не бачу жодного, тому мені здається, що це переслідування. В інших випадках, як-от статус, кольори, бекенд тощо, я можу придумати методи, і саме тому вони є інтерфейсами.
Вінстон Еверт

@WinstonEwert і редагування, здається , що вони є різними типами поведінки.

Ох. Так, якщо я можу придумати метод для перерахунку, то це може бути кандидатом на інтерфейс? Це може бути дуже хорошим моментом.
stromms

1
@stromms, чи можете ви поділитися більше прикладами комутаторів, щоб я міг краще зрозуміти, як ви використовуєте TickType?
Вінстон Еверт

8

При передачі даних переліки не мають запаху коду

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

Я вважаю за краще передати довільну stringsабо ints. Рядки можуть спричинити проблеми внаслідок написання та написання великої літери. Ints дозволяють передавати з діапазону значень і мають мало семантики (наприклад , я отримую 3від вашого торгового обслуговування, що ж це значить? LastPrice? LastQuantity? Що - то ще?

Передача об'єктів та використання ієрархій класів не завжди можливі; наприклад, не дозволяє отримуючому кінці розрізняти, який клас був надісланий.

У моєму проекті сервіс використовує ієрархію класів для ефектів операцій, безпосередньо перед передачею через a DataContract об'єкт з ієрархії класів копіюється в об'єкт- unionподібний, який містить enumвказівку типу. Клієнт отримує DataContractі створює об’єкт класу в ієрархії, використовуючи enumзначення для switchта створюючи об'єкт правильного типу.

Інша причина, через яку не хотілося б передавати об'єкти класу, полягає в тому, що для служби може знадобитися зовсім інша поведінка для переданого об'єкта (наприклад LastPrice), ніж клієнт. У такому випадку надсилання класу та його методів небажана.

Чи погані заяви переключення?

ІМХО, а єдиний switch -statement , який викликає різні конструктори в залежності від enumНЕ код запах. Це не обов'язково краще чи гірше інших методів, таких як база відображення на ім'я типу; це залежить від реальної ситуації.

Увімкнувши перемикачі enum всюди - кодовий запах, пропонує альтернативи, які часто краще:

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

Насправді, TickType передається через провід як int. Моя обгортка - це та, яка використовує, TickTypeяка відлита від отриманого int. У кількох подіях використовується цей тип тексту з дивовижними підписами, які відповідають на різні запити. Чи загальноприйнята практика постійно використовувати intдля різних функцій? наприклад, TickPrice(int type, double value)використовує 1,3 і 6, typeтоді як TickSize(int type, double valueвикористовує 2,4 і 5? Чи є навіть сенс розділяти їх на дві події?
строми

2

що робити, якщо ви перейшли з більш складним типом:

    abstract class TickType
    {
      public abstract string Name {get;}
      public abstract double TickValue {get;}
    }

    class BuyPrice : TickType
    {
      public override string Name { get { return "Buy Price"; } }
      public override double TickValue { get { return 2.35d; } }
    }

    class BuyQuantity : TickType
    {
      public override string Name { get { return "Buy Quantity"; } }
      public override double TickValue { get { return 4.55d; } }
    }
//etcetera

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


1

TL; DR

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

Вибачте, коли я переробляю тонни свого спадкового коду. / зітхання; ^ Д


Але чому?

Досить вдалий огляд, чому це справа від LosTechies тут :

// calculate the service fee
public double CalculateServiceFeeUsingEnum(Account acct)
{
    double totalFee = 0;
    foreach (var service in acct.ServiceEnums) { 

        switch (service)
        {
            case ServiceTypeEnum.ServiceA:
                totalFee += acct.NumOfUsers * 5;
                break;
            case ServiceTypeEnum.ServiceB:
                totalFee += 10;
                break;
        }
    }
    return totalFee;
} 

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

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

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

public interface ICalculateServiceFee
{
    double CalculateServiceFee(Account acct);
}

...

Тепер ми можемо створити наші конкретні реалізації інтерфейсу та приєднати їх [до] облікового запису.

public class Account{
    public int NumOfUsers{get;set;}
    public ICalculateServiceFee[] Services { get; set; }
} 

public class ServiceA : ICalculateServiceFee
{
    double feePerUser = 5; 

    public double CalculateServiceFee(Account acct)
    {
        return acct.NumOfUsers * feePerUser;
    }
} 

public class ServiceB : ICalculateServiceFee
{
    double serviceFee = 10;
    public double CalculateServiceFee(Account acct)
    {
        return serviceFee;
    }
} 

Ще один випадок впровадження ...

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

private static string _getErrorCKey(int statusCode)
{
    string ret;

    switch (statusCode)
    {
        case StatusCodes.Status403Forbidden:
            ret = "BRANCH_UNAUTHORIZED";
            break;

        case StatusCodes.Status422UnprocessableEntity:
            ret = "BRANCH_NOT_FOUND";
            break;

        default:
            ret = "BRANCH_GENERIC_ERROR";
            break;
    }

    return ret;
}

... можливо, я повинен загорнути коди статусу в класи.

public interface IAmStatusResult
{
    int StatusResult { get; }    // Pretend an int's okay for now.
    string ErrorKey { get; }
}

Тоді кожен раз, коли мені потрібен новий тип IAmStatusResult, я кодую його ...

public class UnauthorizedBranchStatusResult : IAmStatusResult
{
    public int StatusResult => 403;
    public string ErrorKey => "BRANCH_UNAUTHORIZED";
}

... і тепер я можу переконатися, що раніше код зрозумів, що він має IAmStatusResultобласть застосування і посилається на нього entity.ErrorKeyзамість більш перекрученого, деденда _getErrorCode(403).

І, що ще важливіше, щоразу, коли я додаю новий тип повернутого значення, для його обробки не потрібно додавати інший код . Whaddya знаю, enumі switch, ймовірно, код пахне.

Прибуток.


Як щодо випадку, коли, наприклад, у мене є поліморфні класи, і я хочу налаштувати, який я використовую через параметр командного рядка? Командний рядок -> enum -> switch / map -> реалізація здається досить законним випадком використання. Я думаю, що ключовим є те, що перерахунок використовується для оркестрації, а не як реалізація умовної логіки.
Ant Ant

@AntP Чому б у цьому випадку не використовувати StatusResultзначення? Ви можете стверджувати, що enum є корисним як ярлик, що запам'ятовується людиною, у тому випадку використання, але я, мабуть, все-таки називатиму цей запах коду, оскільки є хороші альтернативи, які не потребують закритої колекції.
ruffin

Які альтернативи? Рядок? Це довільна відмінність з точки зору "переписки повинні бути замінені поліморфізмом" - будь-який підхід вимагає відображення значення до типу; уникнення перерахунку в цьому випадку нічого не досягає.
Ant Ant

@AntP Я не впевнений, де тут перетинаються дроти. Якщо ви посилаєтесь на властивість в інтерфейсі, ви можете перекидати цей інтерфейс де завгодно, і ніколи не оновлювати цей код, створюючи нову (або видаляючи існуючу) реалізацію цього інтерфейсу . Якщо у вас є enum, ви дійсно повинні коду поновлення кожен раз , коли ви додаєте або видаляєте значення, і потенційно багато місць, в залежності від того, як структурована вашого коду. Використання поліморфізму замість enum - це щось на зразок забезпечення вашого коду в нормальній формі Бойса-Кодда .
ruffin

Так. Тепер припустимо, у вас є N таких поліморфних реалізацій. У статті Los Techies вони просто повторюються через них. Але що робити, якщо я хочу умовно застосувати лише одну реалізацію на основі, наприклад, параметрів командного рядка? Тепер ми повинні визначити відображення від деякого значення конфігурації до якогось типу ін'єкційного впровадження. Перерахунок в цьому випадку такий же хороший, як і будь-який інший тип конфігурації. Це приклад ситуації, коли більше немає можливості замінити «брудний перелік» на поліморфізм. Гілка за допомогою абстракції може зайняти вас лише поки що.
Ant Ant

0

Чи буде використання enum запахом коду чи ні, залежить від контексту. Я думаю, ви можете отримати кілька ідей для відповіді на своє запитання, якщо врахувати проблему з виразом . Отже, у вас є колекція різних типів і набір операцій над ними, і вам потрібно впорядкувати свій код. Є два простих варіанти:

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

Яке рішення краще?

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

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

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

Отже, ви повинні спробувати зрозуміти, в якому напрямку може розвиватися ваша заявка, а потім вибрати відповідне рішення.

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

Підсумок: перерахунки - це не обов'язково кодовий запах.


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

@DaveCousineau: "Я не вважаю, що один є" кращим ", ніж інший, як ви описуєте": я не сказав, що один завжди кращий за інший. Я сказав, що один кращий за інший залежно від контексту. Наприклад , якщо операції більш-менш фіксованими і ви плануєте додавання нових типів (наприклад, графічний інтерфейс з фіксованою paint(), show(), close() resize()операції і призначені для користувача віджети) , то об'єктно-орієнтований підхід краще в тому сенсі , що дозволяє додати новий тип , не зачіпаючи занадто багато існуючого коду (ви в основному реалізуєте новий клас, який є локальною зміною).
Джорджіо

Я не розглядаю як «кращого», як ви описали, що означає, що я не бачу, що це залежить від ситуації. Кількість коду, який ви повинні написати, точно однакова в обох сценаріях. Єдина відмінність полягає в тому, що один із способів антитетичний проти OOP (перерахунків), а другий - ні.
Дейв Кузен

@DaveCousineau: Кількість коду, який ви повинні написати, ймовірно, однакова, місцевість (місця у вихідному коді, які вам доведеться змінити та перекомпілювати) різко змінюється. Наприклад, в OOP, якщо ви додаєте новий метод в інтерфейс, ви повинні додати реалізацію для кожного класу, який реалізує цей інтерфейс.
Джорджіо

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