Создают ли перечисления хрупкие интерфейсы?

17

Рассмотрим пример ниже. Любое изменение в перечислении ColorChoice влияет на все подклассы IWindowColor.

Имеют ли перечисления тенденцию вызывать хрупкие интерфейсы? Есть ли что-то лучше, чем enum для большей полиморфной гибкости?

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, который не может быть расширен в будущем для другого приложения, где допустимые типы символов: {человек, карлик}.

Изменить: Просто чтобы быть более конкретным о том, над чем я работаю. Я проектирую сильную привязку этой спецификации ( MusicXML ) и использую классы enum для представления тех типов в спецификации, которые объявлены с помощью xs: enumeration. Я пытаюсь думать о том, что произойдет, когда выйдет следующая версия (4.0). Может ли моя библиотека классов работать в режиме 3.0 и в режиме 4.0? Если следующая версия на 100% обратно совместима, то возможно. Но если значения перечисления были удалены из спецификации, то я мертв в воде.

Мэтью Джеймс Бриггс
источник
2
Когда вы говорите «полиморфная гибкость», какие именно возможности вы имеете в виду?
Ixrec
3
Использование перечисления для цветов создает хрупкие интерфейсы, а не просто «использование перечисления».
Док Браун
3
Добавление нового варианта enum нарушает код, используя это enum. С другой стороны, добавление новой операции в перечисление совершенно автономно, так как все случаи, которые необходимо обработать, находятся тут же (противопоставьте это интерфейсам и суперклассам, где добавление метода не по умолчанию является серьезным критическим изменением). Это зависит от того, какие изменения действительно необходимы.
1
Re MusicXML: Если нет простого способа определить из файлов XML, какую версию схемы использует каждая из них, это может показаться мне критическим недостатком проекта в спецификации. Если вам нужно как-то обойти это, мы, вероятно, не сможем помочь, пока мы не будем точно знать, что они выберут, чтобы сломаться в 4.0, и вы не сможете спросить о конкретной проблеме, которую это вызывает.
Иксрек

Ответы:

25

При правильном использовании перечисления гораздо более читабельны и надежны, чем «магические числа», которые они заменяют. Обычно я не вижу, чтобы они делали код более хрупким. Например:

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

Однако использование enum для цвета сомнительно, потому что во многих (большинстве?) Ситуациях нет причин ограничивать пользователя таким небольшим набором цветов; Вы можете также позволить им передавать любые произвольные значения RGB. В проектах, с которыми я работаю, небольшой список цветов, подобных этому, может появиться только как часть набора «тем» или «стилей», которые должны выступать в качестве тонкой абстракции над конкретными цветами.

Я не уверен, к чему твой вопрос о "полиморфной гибкости". У перечислений нет исполняемого кода, поэтому нечего делать полиморфным. Возможно, вы ищете шаблон команды ?

Edit: Post-edit, мне все еще не ясно, какую именно расширяемость вы ищете, но я все еще думаю, что шаблон команды - это самая близкая вещь, которую вы получите к «полиморфному перечислению».

Ixrec
источник
Где я могу узнать больше о том, чтобы не разрешить 0 проходить как перечисление?
TankorSmash
5
@TankorSmash C ++ 11 ввел «класс enum», также называемый «enum», который неявным образом преобразуется в их базовый числовой тип. Они также избегают загрязнения пространства имен, как это делали старые типы enum.
Мэтью Джеймс Бриггс
2
Перечисления обычно поддерживаются целыми числами. Есть много странных вещей, которые могут произойти в сериализации / десериализации или приведения между целыми числами и перечислениями. Не всегда безопасно предполагать, что перечисление всегда будет иметь допустимое значение.
Эрик
1
Ты прав, Эрик (и с этой проблемой я сталкивался несколько раз сам). Однако вам нужно беспокоиться только о недопустимом значении на этапе десериализации. Во всех других случаях, когда вы используете перечисления, вы можете предположить, что значение допустимо (по крайней мере для перечислений в таких языках, как Scala - в некоторых языках нет очень строгой проверки типов для перечислений).
Kat
1
@Ixrec "потому что во многих (большинстве?) Ситуациях нет причин ограничивать пользователя таким небольшим набором цветов". Есть законные случаи. .Net Console эмулирует консоль Windows в старом стиле, которая может иметь текст только в одном из 16 цветов (16 цветов стандарта CGA) msdn.microsoft.com/en-us/library/…
Pharap
15

Любое изменение в перечислении ColorChoice влияет на все подклассы IWindowColor.

Нет, это не так. Есть два случая:

  • хранить, возвращать и пересылать значения перечисления, никогда не воздействуя на них, и в этом случае на них не влияют изменения в перечислении, или

  • работать с отдельными значениями перечисления, и в этом случае любое изменение в перечислении, конечно, естественно, неизбежно, обязательно , должно быть учтено с соответствующим изменением логики реализатора.

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

Имеют ли перечисления тенденцию вызывать хрупкие интерфейсы?

Нет, они не Хрупкие интерфейсы вызывают программисты, которые считают себя ниндзя и пытаются скомпилировать свой код без достаточного количества предупреждений. Затем компилятор не предупреждает их о том, что их drawSolid()функция содержит switchоператор, в котором отсутствует caseпредложение для вновь добавленного значения перечисления «Ikositetrahedron».

Способ, которым он должен работать, аналогичен добавлению нового чисто виртуального метода в базовый класс: тогда вы должны реализовать этот метод для каждого отдельного наследника, иначе проект не будет и не должен собираться.


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

Чисто объектно-ориентированный способ делать вещи - не иметь перечислений вообще, а вместо этого иметь объекты целиком.

Итак, чисто объектно-ориентированный способ реализации примера с использованием твердых тел, конечно же, заключается в использовании полиморфизма: вместо того, чтобы иметь один чудовищный централизованный метод, который знает, как рисовать все, и нужно сказать, какое тело рисовать, вы объявляете " Сплошной »класс с абстрактным (чисто виртуальным) draw()методом, а затем вы добавляете подклассы« Сфера »,« Прямоугольник »и« Пирамида », каждый из которых имеет свою собственную реализацию, draw()которая умеет рисовать себя.

Таким образом, когда вы вводите подкласс «Ikositetrahedron», вам просто нужно предоставить draw()для него функцию, и компилятор напомнит вам сделать это, не позволяя создавать экземпляр «Icositetrahedron» в противном случае.

Майк Накис
источник
Какие-нибудь советы по выдаче предупреждения о времени компиляции для этих ключей? Я обычно просто бросаю исключения во время выполнения; но время компиляции может быть отличным! Не совсем так хорошо ... но юнит тесты приходят на ум.
Вон Хилтс
1
Прошло довольно много времени с тех пор, как я последний раз использовал C ++, поэтому я не уверен, но я ожидаю, что предоставление параметра -Wall компилятору и опускание default:предложения должны это сделать. Быстрый поиск по теме не дал более определенных результатов, так что это могло бы подойти как предмет другого programmers SEвопроса.
Майк Накис
В C ++ может быть проверка времени компиляции, если вы ее включите. В C # любая проверка должна быть во время выполнения. Каждый раз, когда я принимаю перечисление, не являющееся флагом, в качестве параметра, я проверяю его с помощью Enum.IsDefined, но это все равно означает, что вам придется обновлять все использования перечисления вручную, когда вы добавляете новое значение. См .: stackoverflow.com/questions/12531132/…
Майк поддерживает Монику
1
Я хотел бы надеяться, что объектно-ориентированный программист никогда не сделает свой объект передачи данных, как на Pyramidсамом деле знает, как draw()пирамиду. В лучшем случае он может быть производным Solidи иметь GetTriangles()метод, и вы можете передать его SolidDrawerслужбе. Я думал, что мы уходим от примеров физических объектов как примеров объектов в ООП.
Скотт Уитлок
1
Все в порядке, просто не понравилось, что кто-то избивал старые добрые объектно-ориентированные программы :) Тем более что большинство из нас больше сочетают функциональное программирование и ООП, чем строго ООП.
Скотт Уитлок
14

Перечисления не создают хрупкие интерфейсы. Злоупотребление перечислениями делает.

Для чего нужны перечисления?

Перечисления предназначены для использования в качестве наборов осмысленно именованных констант. Они должны использоваться, когда:

  • Вы знаете, что никакие значения не будут удалены.
  • (И) Вы знаете, что маловероятно, что потребуется новое значение.
  • (Или) Вы соглашаетесь с тем, что потребуется новое значение, но достаточно редко, чтобы оправдать исправление всего кода, который ломается из-за него.

Хорошее использование перечислений:

  • Дни недели: (согласно .Net's System.DayOfWeek) Если вы не имеете дело с каким-то невероятно малоизвестным календарем, будет только 7 дней недели.
  • Нерасширяемые цвета: (согласно .Net System.ConsoleColor). Некоторые могут не согласиться с этим, но .Net решил сделать это по причине. В консольной системе .Net для консоли доступно только 16 цветов. Эти 16 цветов соответствуют устаревшей цветовой палитре, известной как CGA или «Цветной графический адаптер» . Новые значения никогда не будут добавлены, что означает, что это действительно разумное применение перечисления.
  • Перечисления, представляющие фиксированные состояния: (согласно Java Thread.State) Разработчики Java решили, что в модели потоков Java всегда будет только фиксированный набор состояний, в которых Threadможет находиться a , и поэтому для упрощения эти различные состояния представлены в виде перечислений , Это означает, что многие проверки на основе состояний являются простыми if и переключателями, которые на практике работают с целочисленными значениями, и программисту не нужно беспокоиться о том, какие значения есть на самом деле.
  • Битовые флаги, представляющие не взаимоисключающие параметры: (согласно .Net System.Text.RegularExpressions.RegexOptions) Битовые флаги являются очень распространенным использованием перечислений. Фактически настолько распространенный, что в .Net все перечисления имеют HasFlag(Enum flag)встроенный метод. Они также поддерживают побитовые операторы, и есть FlagsAttributeвозможность пометить перечисление как предназначенное для использования в качестве набора битовых флагов. Используя перечисление в качестве набора флагов, вы можете представлять группу логических значений в одном значении, а также иметь флаги с четкими именами для удобства. Это было бы чрезвычайно полезно для представления флагов регистра состояния в эмуляторе или для представления прав доступа к файлу (чтение, запись, выполнение) или практически для любой ситуации, когда набор связанных опций не является взаимоисключающим.

Неправильное использование перечислений:

  • Классы / типы персонажей в игре: если игра не является демонстрационной демонстрацией из одного кадра, которую вы вряд ли будете использовать снова, перечисления не должны использоваться для классов персонажей, поскольку есть вероятность, что вы захотите добавить намного больше классов. Лучше иметь один класс, представляющий персонажа, и иметь «тип» персонажа в игре, представленный иначе. Одним из способов справиться с этим является шаблон TypeObject , другие решения включают регистрацию типов символов с помощью словаря / реестра типов или их комбинации.
  • Расширяемые цвета: если вы используете перечисление для цветов, которые впоследствии могут быть добавлены, не рекомендуется представлять это в виде перечисления, иначе у вас останется добавление цветов навсегда. Это похоже на проблему, описанную выше, поэтому следует использовать аналогичное решение (т.е. вариант TypeObject).
  • Расширяемое состояние: если у вас есть конечный автомат, который может в конечном итоге ввести гораздо больше состояний, не рекомендуется представлять эти состояния посредством перечислений. Предпочтительный метод состоит в том, чтобы определить интерфейс для состояния машины, предоставить реализацию или класс, который оборачивает интерфейс и делегирует его вызовы методов (сродни шаблону стратегии ), а затем изменяет его состояние, изменяя, какая из реализаций активна в настоящее время.
  • Битовые флаги, представляющие взаимоисключающие параметры: если вы используете перечисления для представления флагов, и два из этих флагов никогда не должны появляться вместе, значит, вы застрелились в ногу. Все, что запрограммировано реагировать на один из флагов, будет внезапно реагировать на тот флаг, на который он запрограммирован реагировать первым - или, что еще хуже, он может реагировать на оба. Такая ситуация просто напрашивается на неприятности. Наилучший подход состоит в том, чтобы рассматривать отсутствие флага как альтернативное условие, если это возможно (т.е. Trueподразумевается отсутствие флага False). Это поведение может быть дополнительно поддержано с помощью специализированных функций (то есть IsTrue(flags)и IsFalse(flags)).
Pharap
источник
Я добавлю примеры битовых флагов, если кто-нибудь сможет найти рабочий или хорошо известный пример перечислений, используемых в качестве битовых флагов. Я знаю, что они существуют, но, к сожалению, я не могу вспомнить ни одного в данный момент.
Pharap
1
RegexOptions MSND.Microsoft.com/en-us/library/
BLSully
@BLSully Отличный пример. Не могу сказать, что когда-либо использовал их, но они существуют по причине.
Pharap
4

Перечисления являются большим улучшением по сравнению с магическими идентификационными номерами для закрытых наборов значений, которые не имеют большой функциональности, связанной с ними. Обычно вас не волнует, какое число на самом деле связано с перечислением; в этом случае его легко расширить, добавив новые записи в конце, хрупкости не должно получиться.

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

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

Один или два из них в порядке, но как только у вас есть такое осмысленное перечисление, эти switchутверждения, как правило, размножаются как трибулы. Теперь каждый раз, когда вы расширяете enum, вам нужно выследить все связанные с ними switches, чтобы убедиться, что вы охватили все. Это хрупко Источники, такие как Чистый код , предполагают, что вы должны иметь не более одного switchна каждое перечисление.

Вместо этого в этом случае следует использовать принципы ОО и создать интерфейс для этого типа. Вы можете по-прежнему сохранять перечисление для связи с этим типом, но как только вам нужно что-то с ним сделать, вы создаете объект, связанный с перечислением, возможно, с использованием Factory. Это гораздо проще в обслуживании, так как вам нужно искать только одно место для обновления: ваш Factory и добавляя новый класс.

congusbongus
источник
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. Есть центральный реестр. Поскольку вы хотите исправить соответствие между именами и значениями, плохо, если вы разрешаете сторонним пользователям вашего кода добавлять новые константы. Ни один проект, использующий ваш код, не должен изменять этот заголовок для добавления новых отображений. Вместо этого они должны добавить вас к ошибке. Это означает, что для упомянутого вами примера человека и дварфа перечисления действительно плохо подходят. Там было бы лучше иметь какой-то реестр во время выполнения, где пользователи вашего кода могут вставить строку и получить уникальный номер, красиво завернутый в какой-то непрозрачный тип. «Число» может даже быть указателем на строку, о которой идет речь, это не имеет значения.

Несмотря на то, что мой последний пункт выше делает перечисления плохо подходящими для вашей гипотетической ситуации, ваша реальная ситуация, когда некоторые спецификации могут измениться, и вам придется обновить какой-то код, кажется, вполне вписывается в центральный реестр вашей библиотеки. Поэтому перечисления должны быть уместны, если вы принимаете мои другие предложения близко к сердцу.

MVG
источник