Чи створюють переліки крихкі інтерфейси?


17

Розглянемо приклад нижче. Будь-яка зміна перерахунку ColorChoice впливає на всі підкласи IWindowColor.

Чи викликають перерахунки крихкі інтерфейси? Чи є щось краще, ніж ентум, щоб дозволити більше поліморфної гнучкості?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

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

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

Далі, уявіть, що обробляється відповідним підкласом ISomethingThatNeedsToKnowWhatTypeOfCharacter, що подається за заводською схемою дизайну. Тепер у мене є API, який не можна розширити в майбутньому для іншого додатка, де допустимі типи символів {human, dwarf}.

Редагувати: Просто щоб бути більш конкретним щодо того, над чим я працюю. Я розробляю сильну прив'язку цієї специфікації ( MusicXML ), і я використовую класи перерахувань для представлення тих типів у специфікації, які оголошені за допомогою xs: перерахування. Я намагаюся подумати про те, що станеться, коли вийде наступна версія (4.0). Чи може моя бібліотека класів працювати в режимі 3.0 та в режимі 4.0? Якщо наступна версія на 100% відстала назад, можливо, можливо. Але якщо значення перерахування були вилучені із специфікації, то я мертвий у воді.


2
Коли ви говорите "поліморфна гнучкість", які можливості ви точно маєте на увазі?
Іксрек


3
Використання enum для кольорів створює крихкі інтерфейси, а не просто "використання enum".
Док Браун

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

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

Відповіді:


25

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

  • setColor () не повинен витрачати час на перевірку, чи valueце дійсне значення кольору чи ні. Компілятор це вже зробив.
  • Ви можете написати setColor (Колір :: Червоний) замість setColor (0). Я вважаю, що enum classфункція в сучасному C ++ навіть дозволяє змусити людей завжди писати перше замість другого.
  • Зазвичай це не важливо, але більшість переліків можуть бути реалізовані з будь-яким інтегральним типом будь-якого розміру, тому компілятор може вибрати будь-який розмір, який є найбільш зручним, не змушуючи вас думати про такі речі.

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

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

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


Де я можу дізнатися більше про те, щоб не допустити 0, щоб перейти як перерахунок?
TankorSmash

5
@TankorSmash C ++ 11 представив "enum class", який також називають "скопійованими перерахунками", який неможливо неявно перетворити на їхній нижчий числовий тип. Вони також уникають забруднення простору імен, як це робили старі типи "enum" у стилі C.
Меттью Джеймса Бріггса

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

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

1
@Ixrec "тому що у багатьох (більшості?) Ситуаціях немає причин обмежувати користувача таким невеликим набором кольорів". Є законні випадки. Консоль .Net імітує консоль Windows в старому стилі, яка може містити текст лише одного з 16 кольорів (16 кольорів стандарту CGA) msdn.microsoft.com/en-us/library/…
Pharap

15

Будь-яка зміна перерахунку ColorChoice впливає на всі підкласи IWindowColor.

Ні, це не так. Є два випадки: реалізатори будуть або

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

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

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

Чи викликають перерахунки крихкі інтерфейси?

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

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


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

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

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

Таким чином, коли ви запровадите підклас "Ікосітетраедр", вам просто потрібно буде надати draw()функцію для цього, і компілятор нагадає вам це зробити, не даючи інстанціювати "Ікосітетраедр" інакше.


Якісь поради щодо викидання попередження про компіляцію часу для цих комутаторів? Я зазвичай просто кидаю винятки з виконання; але час на компіляцію може бути чудовим! Не дуже чудово .. але приходять в голову одиничні тести.
Vaughan Hilts

1
Минуло деякий час, коли я востаннє використовував C ++, тому я не впевнений, але я би сподівався, що постачання параметру -Wall компілятору та опущення цього default:пункту повинно це зробити. Швидкий пошук теми не дав більш певних результатів, тому це може бути придатним як тема іншого programmers SEпитання.
Майк Накіс

У C ++ може бути перевірена час компіляції, якщо ви її ввімкнули. У C # будь-яка перевірка повинна бути на час виконання. Щоразу, коли я беру неперевірений enum як параметр, я обов'язково перевіряю його за допомогою Enum.IsDefined, але це все ще означає, що вам потрібно оновити всі способи використання enum вручну, коли ви додаєте нове значення. Дивіться: stackoverflow.com/questions/12531132/…
Майк підтримує Моніку

1
Я би сподівався, що об'єктно-орієнтований програміст ніколи не зробить їх об’єктом передачі даних, як Pyramidнасправді знає, як draw()піраміда. У кращому випадку це може бути результатом Solidі мати GetTriangles()метод, і ви можете передати його SolidDrawerслужбі. Я думав, що ми відходимо від прикладів фізичних об'єктів як прикладів об'єктів в ООП.
Скотт Вітлок

1
Це нормально, просто не сподобалося, що хтось базує гарне старе об'єктно-орієнтоване програмування. :) Тим більше, що більшість з нас більше поєднує функціональне програмування та OOP більше, ніж суворо OOP.
Скотт Вітлок

14

Енуми не створюють крихких інтерфейсів. Зловживання переліків робить.

Для чого є перерахунки?

Енуми розроблені для використання у вигляді наборів значущо названих констант. Вони повинні використовуватися, коли:

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

Гарне використання переліків:

  • Дні тижня: (за даними .Net System.DayOfWeek) Якщо ви не маєте справу з неймовірним незрозумілим календарем, буде лише 7 днів тижня.
  • Кольори, що не розширюються: (відповідно до .Net System.ConsoleColor) Деякі можуть погодитися з цим, але .Net вирішив зробити це з причини. У консольній системі .Net лише 16 кольорів доступні для використання консоллю. Ці 16 кольорів відповідають застарілій палітрі кольорів, відомій як CGA або "Колірний графічний адаптер" . Ніяких нових значень ніколи не буде додано, це означає, що це насправді розумне застосування перерахунку.
  • Перерахування, що представляють нерухомі стани: (відповідно до Java Thread.State) Дизайнери Java вирішили, що в моделі нарізування Java завжди буде встановлений лише фіксований набір станів, в яких Threadможе бути, і, щоб спростити питання, ці різні стани представлені як перелічення . Це означає, що багато перевірок на основі стану є простими ifs і перемикачами, які на практиці працюють на цілі значення, при цьому програміст не повинен турбуватися про те, якими є насправді значення.
  • Bitflags, що представляють не взаємовиключні варіанти: (відповідно до .Net System.Text.RegularExpressions.RegexOptions), Bitflags - це дуже поширене використання переліків. Насправді настільки поширене, що в .Net всі перелічувачі мають HasFlag(Enum flag)вбудований метод. Вони також підтримують побітові оператори і є, FlagsAttributeщоб позначити перерахунок як такий, який планується використовувати як набір бітфлагів. Використовуючи enum як набір прапорів, ви можете представити групу булевих значень в одному значенні, а також мати прапори, чітко названі для зручності. Це було б надзвичайно вигідно для представлення прапорів реєстру статусу в емуляторі або для представлення дозволів файлу (читання, запис, виконання) або будь-якої ситуації, коли набір пов'язаних параметрів не виключають взаємно.

Погане використання переліків:

  • Класи / типи персонажів у грі: Якщо в грі не буде одномоментна демонстрація, яку ви навряд чи зможете знову використовувати, переписки не слід використовувати для класів символів, оскільки, швидше за все, ви захочете додати набагато більше класів. Краще мати один клас, який представляє символів, а також "тип" гри в іграх, представлений інакше. Одним із способів впоратися з цим є шаблон TypeObject , інші рішення включають в себе реєстрацію типів символів за допомогою реєстру словника / типу або їх суміш.
  • Кольори, що розширюються: Якщо ви використовуєте enum для кольорів, які згодом можуть бути додані до нього, це не гарна ідея представляти це у формі enum, інакше вам назавжди залишиться додавати кольори. Це схоже на вищезазначене питання, тому слід використовувати подібне рішення (тобто варіацію TypeObject).
  • Розширюваний стан: Якщо у вас є машина стану, яка може ввести введення ще багатьох станів, представляти ці стани за допомогою перерахувань недобре. Кращим методом є визначення інтерфейсу для стану машини, надання реалізації або класу, який обгортає інтерфейс і делегує його виклики методу (подібні до шаблону стратегії ), а потім змінює його стан, змінюючи, за умови, що реалізація в даний час активна.
  • Бітфлаги, що представляють взаємовиключні варіанти: Якщо ви використовуєте переписки для представлення прапорів, і два з цих прапорів ніколи не повинні зустрічатися разом, то ви вистрілили собі в ногу. Все, що запрограмовано реагувати на один із прапорів, раптом відреагує на той прапор, який запрограмовано реагувати на перший - або ще гірше, він може відповісти і на обидва. Така ситуація просто вимагає неприємностей. Найкращим підходом є трактування відсутності прапора як альтернативної умови, якщо це можливо (тобто відсутність Trueпрапора означає False). Така поведінка може бути додатково підтримана за допомогою використання спеціалізованих функцій (тобто IsTrue(flags)і IsFalse(flags)).

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


@BLSully Відмінний приклад. Не можу сказати, що я їх коли-небудь використовував, але вони існують чомусь.
Фарап

4

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

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

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

Одне-два з них нормально, але як тільки у вас switchз’являється такий змістовний перелік, ці твердження, як правило, розмножуються, як триблі. Тепер кожного разу, коли ви розширюєте перерахунок, вам потрібно вишукувати всі пов'язані з ними switchес, щоб переконатися, що ви все покрили. Це крихке. Такі джерела, як « Чистий код» , підкажуть, що у вас його має бутиswitch щонайменше на перерахунок.

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


1

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

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

  2. Не робіть на них арифметики. Зокрема, не робіть порівнянь із замовленнями. Чому? Тому що тоді ви можете зіткнутися з ситуацією, коли ви не можете зберегти існуючі значення і одночасно зберегти здорове впорядкування. Візьмемо для прикладу перерахунок для напрямків компаса: N = 0, NE = 1, E = 2, SE = 3,… Тепер, якщо після деякого оновлення ви включите NNE і так далі між існуючими напрямками, ви можете або додати їх до кінця списку і таким чином порушувати впорядкування, або ви переплітаєте їх з існуючими ключами і таким чином розбиваєте встановлені відображення, використані в застарілому коді. Або ви знежираєте всі старі ключі та маєте абсолютно новий набір ключів, а також якийсь код сумісності, який перекладається між старим та новим ключами заради застарілого коду.

  3. Виберіть достатній розмір. За замовчуванням компілятор буде використовувати найменше ціле число, яке може містити всі значення перерахунків. Це означає, що якщо при деякому оновленні набір можливих перерахунків розшириться з 254 до 259, вам раптом знадобляться 2 байти за кожне значення перерахунку замість одного. Це може зруйнувати структуру та класи класів у будь-якому місці, тому намагайтеся уникати цього, використовуючи достатній розмір у першому дизайні. C ++ 11 дає вам багато контролю тут, але в іншому випадку визначає записLAST_SPECIES_VALUE=65535 має допомогти.

  4. Мають центральний реєстр. Оскільки ви хочете виправити відображення між іменами та значеннями, це погано, якщо ви дозволите стороннім користувачам свого коду додавати нові константи. Жоден проект, що використовує ваш код, не повинен дозволяти змінювати цей заголовок, щоб додати нові відображення. Натомість вони повинні попросити вас додати їх. Це означає, що для прикладу людини та карлика, що перелічуються, перерахунки справді погані. Там було б краще мати якийсь реєстр під час виконання, коли користувачі вашого коду можуть вставити рядок і отримати унікальний номер, добре загорнутий у якийсь непрозорий тип. "Число" може бути навіть вказівником на відповідний рядок, це не має значення.

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

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