В последнее время мне часто встречается в коде один интересный шаблон. Он заключается в том, что для описания небольшого множества объектов создается enum, а потом в разных местах кода значения из перечисления обрабатываются при помощи оператора switch.

Как выглядит реализация данного шаблона, и чем он опасен? Давайте разберемся.

Описание задачи


Предположим, команда разрабатывает текстовый редактор и собирается реализовать в нем поддержку нескольких языков программирования. Разумеется не всех, ведь для этого не хватит ресурсов, да и особого смысла в этом не будет.

Для хранения списка поддерживаемых языков создается перечисление

enum Language
public enum Language
{
    Java,
    CSharp,
    TSQL
}


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

GetExtensions(Language lang)
List<string> GetExtensions(Language lang)
{
    switch (lang)
    {
        case Language.Java:
            {
                List<string> result = new List<string>();
                result.Add("java");
                return result;
            }
        case Language.CSharp:
            {
                List<string> result = new List<string>();
                result.Add("cs");
                return result;
            }
        case Language.TSQL:
            {
                List<String> result = new List<string>();
                result.Add("sql");
                return result;
            }
        default:
            throw new InvalidOperationException("Язык " + lang + " не поддерживается");
    }
}


IsCaseSensitive(Language lang)
bool IsCaseSensitive(Language lang)
{
    switch (lang)
    {
        case Language.Java:
        case Language.CSharp:
            return true;
        case Language.TSQL:
            return false;
        default:
            throw new InvalidOperationException("Язык " + lang + " не поддерживается");
    }
}


GetIconFile(Language lang)
string GetIconFile(Language lang)
{
    switch (lang)
    {
        case Language.Java:
            return "bean.svg";
        case Language.CSharp:
            return "cs.svg";
        case Language.TSQL:
            return "tsql.svg";
        default:
            throw new InvalidOperationException("Язык " + lang + " не поддерживается");
    }
}


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

В результате поначалу возникает достаточно простая картина. Все поддерживаемые языки собраны в одно место. Там, где надо определить что-то, зависящее от языка, вставляется switch. Одному разработчику реализовать поддержку 2-3 языков одновременно очень легко. Но вот впоследствии с поддержкой и развитием программы, основанной на этом шаблоне, будут серьезные проблемы.

Недостатки использования enum-switch


Дело в том, что такой подход создает god-объекты (god object). Сам enum и каждый switch играют роль god-объектов. Любое изменение, связанное с одним из поддерживаемых редактором языков программирования, потребует внесения изменения во все god-объекты. Работая над поддержкой Java, можно сломать код, относящийся к C# или TransactSQL.

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

Трудоемкость добавления поддержки нового языка будет постоянно расти, потому что будет увеличиваться размер операторов switch, и контролировать их будет все сложнее и сложнее. Такую программу нельзя будет назвать качественной, потому что хорошие программы должны со временем становиться лучше, проще и дешевле в развитии и поддержке.

При помощи подхода enum-switch разработчики обединяют жесткими связями сущности, которые в реальности между собой практически не связаны. Между TransactSQL и Java вообще может не быть ничего общего кроме того, что кто-то захотел открыть их в одном текстовом редакторе. Но в коде программы TransactSQL и Java оказались в одном типе enum.

Это проявление антипаттерна god-object.

Однако в данном шаблоне можно обнаружить проявление и других антипаттернов. Разработчики текстового редактора не участвуют в разработке языков программирования, они лишь занимаются реализацией логики своего собственного программного продукта. Следовательно, для редактора особенности языков — это внешние данные, которые он должен уметь обрабатывать. Здесь же эти данные являются частью кода. Т. е. получился своеобразный хардкодинг. Если выйдет Java, в которой файлы исходников будут иметь расширение из одной буквы J, то придется переделывать редактор и проверять, не сломались ли остальные языки.

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

Оператор switch зачастую задает связь между сущностями из разных множеств. В нашем примере это связь между языком программирования и иконкой. Однако связь между сущностями — это тоже сущность, и с ней надо обращаться как со всеми остальными данными, например, сохранить в столбце таблицы. Если же нет возможности использовать внешнее хранилище, то хотя бы записать связь в Dictionary.

Dictionary
Dictionary<Language, string> icons = new Dictionary<Language, string>();
icons[Language.Java] = "bean.svg";
icons[Language.CSharp] = "cs.svg";
icons[Language.TSQL] = "tsql.svg";


При этом у оператора switch есть еще один неприятный побочный эффект. Он не только задает связь между объектами, но и сам является связью. Чтобы было понятно, о чем речь, рассмотрим такой пример:

switch (lang)
{
    case Language.TSQL:
    case Language.PLSQL:
        return "sql.svg";
...
}

Двум диалектам SQL поставлена в соответствие иконка sql.svg. Теперь у языка не только есть иконка, но и есть неявное свойство, обозначающее, что у языков TransactSQL и PL-SQL иконки должны быть одинаковыми. Разработчик, который захочет поменять иконку для PL-SQL будет решать вопрос, стоит ли ему менять иконку и для TransactSQL. В большинстве случаев это нежелательно.

И наконец, антипаттерн enum-switch способствует проявлению ошибки типа «Данное значение из enum не предусмотрено», потому что сложно проконтролировать при добавлении нового значения в enum полное покрытие во всех операторах switch.

Выход есть


Как же следует поступать, чтобы избежать использование данного шаблона?

В любой непонятной ситуации заводите интерфейс. Чтобы не использовать enum, заведите интерфейс. Интерфейс должен возвращать информацию о свойствах объекта из описываемого множества. Добавьте в этот интерфейс имя, которое раньше хранилось в константе в enum.

Интерфейс
interface Language
{
    string GetName();
    bool IsCaseSensitive();
    string GetIconFile();
    List<string> GetExtensions();
}


Создание конкретных объектов, реализующих данный интерфейс, поручите отдельному классу-провайдеру.

Провайдер
class LanguageProvider
{
    List<Language> GetSupportedLanguages() {
        ...
    }
    Language DetectLanguageByFile(string fileName) {
        ...
    }
    Language GetDefaultLanguage() {
        ....
    }
}


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

Теперь все функции, содержащие switch, если они у вас были, удалите. Они вам больше не понадобятся, потому что код обрабатывает не конкретные объекты, а их свойства.

В приведенном примере, после того как текстовый редактор будет поддерживать 10-15 различных языков программирования, добавление еще одного языка будет сводиться к перечислению настроек из списка уже реализованных ранее. Ведь, хотя языков программирования существует очень много, большинство нюансов, влияющих на редактирование исходников, у них общие.

Зачем нужен enum


Зачем же все-таки в большинстве языков программирования существует такой тип как enum?

Использовать его удобно, если делать это с определенной осторожностью. Прежде всего enum можно применить там, где количество объектов небольшое. Допустимый предел каждый разработчик определяет по своему усмотрению. Я бы не стал объединять в enum более 20 констант.

Описываемое множество должно состоять из объектов, отличия между которыми могут быть параметризованы. Например, дни недели отличаются друг от друга только порядковым номером, поэтому они хорошо описываются через enum. А вот какие-нибудь погодные явления перечислять в enum-е скорее не стоит, потому что у них очень мало общего.

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

Характерные примеры применения enum:

  • enum Boolean {True, False}
  • дни недели, месяца
  • состояния конечного автомата
Поделиться с друзьями
-->

Комментарии (64)


  1. kekekeks
    17.10.2016 00:08
    +3

    Выбрасывать исключение в конце вынуждает компилятор, которому нужно обеспечить гарантированный результат каждой функции, а проконтролировать полноту покрытия множества значений оператором switch компилятор C# не может.
    Лечится плагином-анализатором к рослину. Зачем из этого целую проблему раздувать и обзывать антипаттерном — не ясно.


    1. centur
      17.10.2016 08:35
      +2

      Это "объектная" ориентация. Все в этом мире — объект. Потом чешут репу и удивляются обилию аллокаций и GC.
      Хотя мне кажется тут автор пропустил один важный компонент — не хватает абстрактной фабрики провайдеров языков. Ну чтобы уж точно добить бедный простой switch-case. А то как-то предложенное решение не до конца "объектно" ориентированно.


      1. sergey-b
        17.10.2016 23:07

        Гораздо более удивляет, когда добавление какой-нибудь безобидной штуки, в которой нет ничего нового, программу приходится полностью перерабатывать, долго тестировать, и все равно в продакшене лезут ошбики.


    1. sergey-b
      17.10.2016 23:22

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


      1. kekekeks
        18.10.2016 13:46

        Есть понятие закрытого множества объектов (discriminated union в F#, case class в Scala, variant в Nemerle). И вам надо гарантировать, что все элементы этого закрытого множества будут обработаны, чего компилятор C# из коробки не обеспечивает, но что можно к нему без всяких проблем добавить посредством анализатора.

        Остальные проблемы являются либо надумаными, либо связаны с неумением мыслить в функциональном стиле и писать/поддерживать функциональный код.


        1. sergey-b
          18.10.2016 21:47

          А как можно обеспечить совместную работу разработчиков над разными объектами и объединение их изменений?


  1. lair
    17.10.2016 00:16
    +2

    Чтобы не использовать enum, заведите интерфейс. Интерфейс должен возвращать информацию о свойствах объекта из описываемого множества. [...] Теперь все функции, содержащие switch, если они у вас были, удалите. Они вам больше не понадобятся, потому что код обрабатывает не конкретные объекты, а их свойства.

    Круто. Теперь представьте себе, что у меня есть внешний по отношению к этому интерфейсу код (т.е., интерфейс и его реализации лежат в библиотеке, а мой код просто эту библиотеку потребляет), и мне надо расширить поведение — например, мне надо в зависимости от пары языков предоставить конвертер из одного языка в другой. Ваши предложения?


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


    состояния конечного автомата

    Адвокат дьявола просит напомнить вам про паттерн State.


    P.S.


    проконтролировать полноту покрытия множества значений оператором switch компилятор C# не может.

    Почему не может-то?


    1. Delphinum
      17.10.2016 10:52
      +2

      А почему у вас бизнес-логика и ее модель лежат в библиотеке?


      1. lair
        17.10.2016 11:19
        -1

        Во-первых, в статье нет ограничения, что этот подход применим только к бизнес-логике.


        Во-вторых, а почему нет? Даже оставаясь в рамках примера из статьи, можно легко увидеть такой сценарий: предположим, что у "редактора" плагинная структура, интерфейсы определены в ядре, каждый язык — плагин, и вся дополнительная функциональность — тоже плагин. Соответственно, если я хочу сделать плагин с конвертерами из языка в язык, то для меня и интерфейсы будут в библиотеке (ядре), и языки будут в библиотеках (плагинах), и совершенно не обязательно они будут мне подконтрольны. И это далеко не единственный сценарий.


        1. Delphinum
          17.10.2016 11:22
          +1

          Vim — редактор с плагинной структурой. Каждый язык это абстракция с единственным свойством filetype (имя). Все элементарно.


          1. lair
            17.10.2016 11:26

            Чем этот filetype отличается от перечисления (кроме того, что он строковый)? Как я могу применить к нему описанную в статье методику "вытащите интерфейс"?


            1. Delphinum
              17.10.2016 11:31
              +1

              Перечисление это объект, который обрабатывается через if/else или switch, а filetype это свойство языка и обрабатывается оно через полиморфизм. При замене одного языка на другой, подгружается одноименный файл-обработчик со всей логикой. Нет нужны в перечислении.

              «Вытащите в интерфейс» методика уже применена.


              1. lair
                17.10.2016 11:56
                -1

                а filetype это свойство языка и обрабатывается оно через полиморфизм.

                Вы под "свойством языка" понимаете свойство (property) объекта, описывающего язык, который поддерживается редактором? А какого это свойство типа?


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

                Вот только у меня не замена одного языка на другой. Мне надо реализовать код, который в зависимости от двух выбранных языков ("из" и "в") будет вызывать ту или иную конверсию.


                «Вытащите в интерфейс» методика уже применена.

                В коде редактора, но не в коде моего плагина.


                1. Delphinum
                  17.10.2016 12:04

                  Под свойством языка я понимаю свойство языка. Filetype это его программное представление string типа.

                  Мне надо реализовать код, который в зависимости от двух выбранных языков («из» и «в») будет вызывать ту или иную конверсию

                  Ну это же просто, ваш плагин должен реализовать некую функцию вида convert(langA, langB) с передачей ему имен целевых языков (или их объектов). Чтоб было понятнее, я приведу другой пример из реальной практики. Я реализовывал плагин для работы с системами deploy в Vim. Разумеется онных десятки, потому я сначала реализовал абстрактный плагин vim_deploy, описывающий семантику всех систем деплоя, а затем для каждой конкретной системы реализовывал конкретный адаптер в контексте описанной семантики. Если адаптер-плагину необходимо будет расширить семантику системы деплоя, он просто реализует новые методы, но использовать их полиморфно уже не получиться. Если сравнивать это решение с enum, то последний имеет ряд недостатков, основным из которых является применение перечисления для представления бесконечного множества.


                  1. lair
                    17.10.2016 12:06

                    Ну это же просто, ваш плагин должен реализовать некую функцию вида convert(langA, langB) с передачей ему имен целевых языков (или их объектов).

                    Именно. Как внутри этой функции мне выбрать нужное преобразование?


                    1. Delphinum
                      17.10.2016 12:09

                      Если семантика объекта-языка позволяет, то можно использовать полиморфизм, иначе лучше использовать некий объект-конвертер, как это реализовано в C++ с перегрузкой операторов (логика преобразования из языка langA в язык langB заложена в объекте класса langA или langB).


                      1. lair
                        17.10.2016 12:11

                        Если семантика объекта-языка позволяет, то можно использовать полиморфизм

                        Полиморфизм — это хорошее красивое слово, но можно конкретный пример?


                        иначе лучше использовать некий объект-конвертер, как это реализовано в C++ с перегрузкой операторов (логика преобразования из языка langA в язык langB заложена в объекте класса langA или langB).

                        Напомню, что у меня нет контроля ни за реализацией langA, ни за реализацией langB.


                        1. Delphinum
                          17.10.2016 12:14

                          Я же уже привел пример с системой деплоя. Вы реализуете класс LangAConverter с методом convertToB; либо, если семантика позволяет, выделяете из объекта языка (полиморфно) необходимые и достаточные свойства и используете их для преобразования в соответствующие свойства целевого языка. Все зависит от семантики представления языка. Если она этого не позволяет, возможно вам следует для вашего плагина реализовать собственную семантику (что вполне приемлемо, ибо цели разные) и работать с ней.


                          1. lair
                            17.10.2016 12:22

                            Вы реализуете класс LangAConverter с методом convertToB

                            Как мне выбрать нужный LangAConverter? Далее, учитывая, что у меня есть n целевых языков — вы предлагаете мне писать по методу на каждый? И выбирать их… как? Или все-таки convertTo(lang)? Тогда мы возвращаемся к вопросу "как выбрать нужную функцию преобразования внутри convertTo".


                            Если она этого не позволяет, возможно вам следует для вашего плагина реализовать собственную семантику

                            Круто. Теперь у меня есть две разных семантики (язык и конвертер), которые зависят от одного и того же множества значений (а именно — множества значений свойства filetype), и мне предстоит милое веселье поддерживать согласованными элементы этих множеств.


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


                            1. Delphinum
                              17.10.2016 12:50

                              Как мне выбрать нужный LangAConverter?

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

                              Теперь у меня есть две разных семантики

                              Нет, семантика у вас одна «Преобразование языка», и она не включает множества, ибо множество в данном случае — зло.

                              Вот пример семантики:
                              interface Lang{
                              string getName()

                              // other properties
                              }

                              interface Convertable extends Lang{
                              Converter getConverter(Lang to)
                              }

                              interface Converter{
                              Lang convert()
                              }


                              Вот реализация через локатор:
                              class JavaToCConverter implements Converter{
                              // логика преобразования
                              }

                              class Java implements Convertable{
                              private converterMap = [
                              C: JavaToCConverter
                              ]

                              Converter getConverter(Lang to){
                              return new this.converterMap[to.getName()]
                              }
                              }

                              class C implements Lang{
                              }


                              Вот реализация через формирование имени конвертера:
                              class Java implements Convertable{
                              Converter getConverter(Lang to){
                              converterName = "JavaTo" + to.getName() + "Converter";
                              if(!classExists(converterName)){
                              throw new Exception;
                              }

                              return new converterName;
                              }
                              }


                              Вот полиморфная реализация:
                              class Java implements Convertable{
                              Converter _getConveter(C to){
                              return new JavaToCConveter;
                              }

                              Converter getConverter(Lang to){
                              return this._getConverter(to)
                              }
                              }


                              Вариантов много, но все сводятся к одной идее: перечисления служат для конечных множеств.


                              1. lair
                                17.10.2016 13:02
                                +1

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

                                Ну то есть, фактически, взять словарь.


                                она не включает множества, ибо множество в данном случае — зло.

                                У любого типа есть множество допустимых значений. Так что множества неизбежно будут использоваться.


                                Вариантов много, но все сводятся к одной идее: перечисления служат для конечных множеств.

                                Спасибо, я вроде бы, ровно то же самое и написал.


                                Более того, множество языков, которые поддерживает конкретный конвертер — как раз конечно (просто вы выражаете его либо через конечное множество элементов в локаторе, либо через конечное множество методов на классе). Но при этом оно очень плохо поддается статической верификации (вплоть до проблемы "автор реализации для C взял и поменял Name, весь код рассыпался в рантайме).


                                1. Delphinum
                                  17.10.2016 13:08

                                  Ну то есть, фактически, взять словарь

                                  Возможностей множество, я привел примеры.
                                  Так что множества неизбежно будут использоваться

                                  Естественно, но они уже конечны, а множество всех языков бесконечно. В этом то и проблема.
                                  Спасибо, я вроде бы, ровно то же самое и написал

                                  Я не ищу правого, я ищу истину. Для меня наш диалог это критичный монолог )
                                  автор реализации для C взял и поменял Name, весь код рассыпался в рантайме

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

                                  Статическая верификация это проблема статического верификатора. Если решение легко тестировать, то все в порядке.


                                  1. lair
                                    17.10.2016 13:13

                                    Естественно, но они уже конечны, а множество всех языков бесконечно

                                    Только теоретически. Практически вы всегда имеете дело с конечными множествами, вопрос только в том, кто именно контролирует элементы множеств.


                                    Связь неизбежна, но мы хотя бы ослабляем ее.

                                    А хорошо ли это? Ослабив связь, вы ухудшили валидируемость решения.


                                    Статическая верификация это проблема статического верификатора. Если решение легко тестировать, то все в порядке.

                                    Ключевое слово — "если". Но в холивар "статическая верификация vs тестирование" я точно вступать не хочу.


                                    1. Delphinum
                                      17.10.2016 13:19

                                      Практически вы всегда имеете дело с конечными множествами

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

                                      Почему? На мой взгляд валидируемость решения не пострадает если есть тесты. Я привык валидировать решения тестами, а не статическим анализатором (но не считаю, что последнее решение бесполезно или вредно).
                                      Ключевое слово — «если»

                                      Ну без «если» никуда )


                1. Sirikid
                  18.10.2016 10:45

                  А какое решение вы предлагаете, type classes?


                  1. lair
                    18.10.2016 10:51

                    Зависит от того, что язык поддерживает.


                    Я, в среднем, считаю, что перечисления неплохи, а типы-суммы удобнее перечислений (но имеют все те же проблемы поддерживаемости). С type classes я не работал, поэтому ничего не могу сказать.


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


    1. sergey-b
      17.10.2016 22:48
      -1

      Почему не может-то?


      Я не знаю. Мне, зашедшему в гости из Java, это непонятно. Возможно, мои друзья из мира C# не все мне рассказали.


      1. lair
        17.10.2016 22:51
        -1

        Эээ, а зачем вы тогда пишете про язык, который вы знаете по рассказам друзей?


        1. sergey-b
          17.10.2016 22:59
          -2

          Чтобы получить поддержку профессионального сообщества и больше узнать по интересующей теме.


    1. 0xd34df00d
      18.10.2016 02:56
      +2

      В функциональном программировании компилятор при этом может убедиться, все элементы множества рассмотрены или не все. Компилятор C#, судя по необходимости ветки по умолчанию, этого сделать не может.

      А для расширения поведения достаточно, если в терминах ФП, уметь проецировать базовый Language на различные конструкторы одного и того же типа и потом визитором по ним, визитором.

      А вообще в ФП канонично не только

      data Language = Java | CSharp | TSQL
      но скорее
      
      {-# LANGUAGE DataKinds, KindSignatures #-}
      
      class Language (a :: Lang) where
          icon :: Proxy a -> String
          isCaseSensitive :: Proxy a -> Bool
          extensions :: Proxy a -> [String]
      
      data Lang = Java | CSharp | TSQL
      
      instance Language Java where
          icon _ = "bean.svg"
          ...
      


      Работать параллельно разным программистам легко: все инстансы счастливо описываются в своих собственных файликах, не придется учиться пользоваться merge tool.
      Расширять языки легко: достаточно описать новый data-тип и инстанс к нему.
      Расширять функциональность как у вас в примере легко: просто пишете новую функциональность.
      Расширять интерфейс легко:
      
      class Language a => CompiledLanguage a where
          compilerName :: Proxy a -> String
      


      Получается такой гибрид енамов и интерфейсов, короче.

      Можно даже без DataKinds, на самом деле, если сделать type erasure через forall, и если для бизнес-логики этого будет достаточно.


      1. pawlo16
        18.10.2016 07:51
        -3

        Класс типов там, где достаточно простой алгебры — как это по-хаскельски! Сделать всё максимально сложно!


  1. MrCheater
    17.10.2016 00:48
    +2

    В некоторых языках программирования, например в Haxe, использование связки enum-switch считается рекомендованным подходом http://haxe.org/manual/types-enum-using.html И про это пишут в документации


    1. Sirikid
      17.10.2016 05:37

      Перечисления в C# и Haxe сильно различаются:
      > Haxe provides powerful enumeration (short: enum) types, which are actually an algebraic data type (ADT).
      Конечно все любят pattern matching вместе с ADT, но в C# switch и перечисления пришли из C вместе с сопутствующими недостатками.
      К слову в Java/Kotlin перечисления это такие же наборы констант, но не чисел а синглтонов, и могут иметь методы и реализовывать интерфейсы, что позволяет избежать описанного антипаттерна.


  1. J_K
    17.10.2016 02:00
    +4

    Все, что вы здесь сделали, называется простым словом "рефакторинг", и прием, описанный Фаулером — "экстрагирование классов". Если перечисления нужны именно как перечисления, без подключения тонны дополнительной логики, пусть они так и останутся перечислениями. Для более сложных случаев подойдут другие решения, например, ваше.


    1. sergey-b
      17.10.2016 22:50

      Согласен. Если нетрудно, дайте ссылку на Фаулера.


      1. J_K
        18.10.2016 03:51

        https://www.ozon.ru/context/detail/id/1308678/

        Или читайте в первоисточнике:
        https://www.csie.ntu.edu.tw/~r95004/Refactoring_improving_the_design_of_existing_code.pdf


  1. Virviil
    17.10.2016 04:44

    Мне кажется, или идея менять enum на interface написана где-то на перых полутора страницах книжки Design Patterns?


    1. samizdam
      17.10.2016 12:18

      Как и любое повторяющееся логическое ветвление, описанный случай со switch — классический запашок кода, где пора использовать полиморфизм.


    1. sergey-b
      17.10.2016 23:59

      Вам не кажется.


  1. Oxoron
    17.10.2016 10:13

    Пример GetExtensions(Language lang) сокращается до
    List<string> GetExtensions(Language lang)
    {
        List<string> result = new List<string>();
        switch (lang)
        {
            case Language.Java:
                {                
                    result.Add("java");
                }
            case Language.CSharp:
                {
                    result.Add("cs");
                }
            case Language.TSQL:
                {
                    result.Add("sql");
                }
            default:
                throw new InvalidOperationException("Язык " + lang + " не поддерживается");
        }
        return result;
    }
    

    Плюс, скобки фигурные можно удалить (хоть это и чревато).


  1. Sioln
    17.10.2016 12:05
    +3

    Тут не enum плох сам по себе, а показан пример именно плохого дизайна при помощи enum.
    Выглядит так, как будто PK из таблички вынесли в код.

    В последнее время мне часто встречается в коде один интересный шаблон.
    Наверное, говорит, что в последнее время вы имеете дело с кодом одного и того же специалиста.


  1. khim
    17.10.2016 13:36
    +1

    Я сделать как «у порядочных сволочей» нельзя? В смысле — засунуть все эти методы в сам enum и бить «палкой по рукам» людей, которые будут напрямую на варинты enum'а ссыслаться вне его? В C# синтаксис, конечно, не так удобен, как в Java, но и не то, чтобы уж прямо ужасен.

    После того, как вы это сделаете неожиданно весь пафос про «множество перечисляемых объектов должно быть или фиксированным, в котором уже не появится новых значений, или внутренним, которое полностью определяется и используется только внутри одной программы» неожиданно исчезает: если enum сам про себя умеет отвечать всё, что нужно — то, собственно, почему бы и нет?


    1. Oxoron
      17.10.2016 14:18

      Проблема тут в

      бить «палкой по рукам» людей, которые будут напрямую на варинты enum'а ссыслаться вне его


      Надо еще отыскать людей, которые делают сие непотребство. А для этого вам нужен внешний инструмент. То есть, либо настроить проверку R#, либо что-то вроде PVS, либо свой анализатор. И я не уверен, что первые два работают такое в билд процессе.


      1. khim
        17.10.2016 16:18
        +1

        Если у вас в проекте есть люди, которые не соблюдают вещи, явно написанные в Style Guide'е, то вам никакой рефакторинг не поможет.


        1. Oxoron
          17.10.2016 17:19

          Вы предлагаете вообще запретить прямые ссылки на enum? Или вносить в Style Guide каждый enum по отдельности?
          Кстати, те же анализаторы эффективнее Style Guid'ов: машина отлавливает ошибки стабильнее ревьюеров (и затрат сил меньше).


          1. khim
            18.10.2016 22:39

            Style Guide, в общем-то, не машина интерпретирует. Запретить делать «switch» для enum'ов, которые могут в будущем получить новые члены.

            Довольно очевидно, что enum из дней недели и через 100 лет будет 7 членов содержать, а вот список поддерживаемых языков — может меняться.


            1. Sirikid
              18.10.2016 23:18
              -1

              64кб хватит всем?


            1. Oxoron
              19.10.2016 10:29

              Окей, у вас есть два десятка перечислений в решении. Для 10 из них switch разрешен, для 10 нет. Программист получает таск и понимает: есть значение AnyEnum, в зависимости от него надо выполнять разные действия.
              Если я правильно вас понял, вы предлагаете ему открыть StyleGuide, найти там страничку с перечислениями, и оттуда узнать, перебираемый этот enum или нет. Вы не находите, что это несколько неудобно?
              Но даже если мы вынесли информацию о переборности в комментарии\атрибуты — программисту надо чекнуть enum, ревьюеру надо чекнуть enum. Это снова неудобно, особенно ревьюеру, а значит вполне возможна ситуация «Наверняка это перебирается» + «Раз Вася поставил — значит можно». В итоге мы имеем баг, который через годик прострелит кому-то колено. И виновникам бага палкой по рукам не настучать — они уже в другой конторе (хотя настучать очень хочется).
              То есть, разделение enum по «перебираемое»\«неперебираемое» без автоконтроля не защищает от ошибок, да и автоконтроль еще надо настроить. Хотя варианта лучше\проще все равно нет.


    1. sergey-b
      17.10.2016 22:54

      Enum в Java сделан гораздо лучше, и там его область применения гораздо шире. Хотя все равно остается класс, который должен содержать в себе данные обо всех.


    1. Deosis
      18.10.2016 07:05

      Ваш пример — это промежуточный вариант: все выборы сосредоточены в одном месте.
      Это убирает главный минус анти-паттерна switch — необходимость искать по всему коду.
      Однако необходимость вносить изменения во все ветки остается.
      При этом если в одном языке появится состояние(например версия для определения списка ключевых слов), то придется передавать все состояния все поддерживаемых языков в эти методы.
      И базового интерфейса есть другой недостаток: при расширении функциональности (те же ключевые слова), придется вносить изменения в различных местах, не все из которых контролируются (плагины)
      ПС. Если планируется расширение перечисления, то это очень серьезный кандидат на выделение интерфейса. Пока switch'и не расползлись по коду.


  1. Adgh
    17.10.2016 22:55

    Ветвления. Что с ними можно сделать
    https://habrahabr.ru/post/112123/

    только вместо C# — C++


    1. sergey-b
      17.10.2016 22:55

      Спасибо.


  1. shaman_timon
    17.10.2016 22:56
    +1

    Со статьей, в целом, я согласен. Однако мой опыт отличается от Вашего, нынче наблюдаю обратный процесс, люди начитавшись книг по ООП(что само по себе и неплохо) начинают выделять интерфейсы и творить «энти фабрики, стратегии» там где это излишне, так ещё для большей расширяемости интерфейс выкидывают в отдельную либу, и мы имеем пятьсотмиллионов проектов). Если расширяемость в данном месте не предполагается, можно и со switch начать, а далее перескочить недолго(если конечно не доводить до портянок в десяток case, которые используются в десятке мест).


    1. sergey-b
      17.10.2016 23:34

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


      1. khim
        18.10.2016 22:43
        +1

        Есть простой тест. Задёте себе вопрос: кто пострадает, если этой абстракции вот прямо тут не станет. Если ответ — «дядя Вася из соседнего отдела»… идёте говорить с Васей. Если ответ — «ну… кто-нибудь, когда-нибудь, в будущем, фиг знает когда»… выкорчёвываете абстракцию нафиг.

        У любой проблемы (а лишний слой абстракции — это проблема всегда, хотя иногда и вынужденная) должно быть имя.


  1. oldcastor
    17.10.2016 22:56

    Впервые работаю над приложением с постоянными веб запросами на каждый чих, после некоторых метаний остановился на варианте с перечислением для запросов (язык java, применение в android). Имеется интерфейс, в нем перечисление запросов в параметризованном виде с доп информацией, которая разбирается при выполнении запроса. Выглядит это так:

    interface QueryParams {
      String queryGET = "GET";
      String queryPOST = "POST";
      int queryGroup_list = 1;
      int queryGroup_details = 2;
      int queryGroup_default = 0;
    
      enum queryParams {
        req_login("login?phonenumber=%1$s&password=%2$s", queryGET, queryGroup_default),
    
        req_TV_orders("get_naryad_list?tehnology=1", queryGET, queryGroup_list),
        req_PHONE_orders("get_naryad_list?tehnology=2", queryGET, queryGroup_list),
    ...
      }
    }
    

    Вызов через асинкТаск класс, например, для логина выглядит так
    new Async_DB_work(
    context, 
    queryParams.req_login, 
    mHandler, 
    findViewById(R.id.progress_overlay)
    ).execute("логин", "пароль");
    

    В самом классе есть метод, который из queryParams.req_login берет строку-запрос (через стринг билдер в %1$s и %2$s подставляются логин и пароль) и метод запроса и отправляет всё это добро на вебсервис.

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

    Можно ли как-то облагородить схему?


    1. sergey-b
      17.10.2016 23:49

      Это код на Java? На Java есть очень строгое соглашение о стиле, для начала надо код привести в соответствие этому стилю. Большинство редакторов помогают это делать при помощи подсветки и подсказок.

      Я бы сделал так
      Уберите интерфейс QueryParams. Константы, если они нужны, объявите через static final внутри enum QueryParams (enum должен называться с большой буквы).

      Создайте Map, в котором ключом будет группа, а значением набор объектов из enum. Заполнение Map будет простой последовательностью вызовов put, а вместо switch будет один вызов метода get.

      Map<Group, Collection<QueryParams>> grouping = new HashMap<>();
      for (QueryParams queryParams: QueryParams.values()) {
          Collection<QueryParams> queryParamsList = grouping.get(queryParams.getGroup());
          if (queryParamsList == null) {
              queryParamsList = new ArrayList<QueryParams>();
              grouping.put(queryParams.getGroup());
          }
          queryParamsList.add(queryParams);
      }
      
      //...
      
      Collection<QueryParams> queryParamsList = grouping.get(group);
      


  1. pawlo16
    18.10.2016 00:25

    Фреймворк, интерфейс, провайдер — унылое императивное ООП. Не надо этого. В F#-пе я использую примерно такой подход:

    type Language =
        | Java
        | CSharp
        | TSQL
        | CustomLang of LangInfo
    
        static member info = function
            | Java ->   LangInfo.new' "java"  true "bean.svg"
            | CSharp -> LangInfo.new' "cs" true "cs.svg"
            | TSQL  ->  LangInfo.new' "sql" false "tsql.svg"
            | CustomLang x -> x
    
    module Helpers = 
        let langConfig : LangInfo list = 
            // взять конфигурацию из внешнего источника
            ...
    
    type Language with
        static member values =         
            [ Java; CSharp; TSQL ] @ (List.map CustomLang Helpers.langConfig )
    


    1. lair
      18.10.2016 00:58

      … и что происходит, когда добавляется новый язык? Он попадает в CustomLang? А смысл тогда первые три значения вводить?


      1. pawlo16
        18.10.2016 07:38
        +1

        == и что происходит, когда добавляется новый язык? Он попадает в CustomLang?

        смотря что проще — внести его в конфиг, либо рассматривать как «особый случай»

        == смысл тогда первые три значения вводить?

        для упрощения обработки «особых случаев», которые по мнению автора не нужны. В предметной области описания ЯП их немерено.

        type Language with
            // F# - наличие провайдеров типа 
            static member hasTypeProviders = function
                | FSharp -> true
                | _ -> false
        
            // SQL, Clojure - наличие транзакций
            static member hasTransactions = function
                | TSQL | MySQL | Clojure -> true
                | _ -> false
        

        Глупо добавлять эти опции в LangInfo в данном случае, иначе этот тип рискует разрастись до абсурдных размеров.


        1. lair
          18.10.2016 10:22

          смотря что проще — внести его в конфиг, либо рассматривать как «особый случай»

          Вот внесли вы его в "особые случаи" (т.е., кейсы в case class), и что дальше случилось со всем кодом, который использовал этот кейс-класс?


          для упрощения обработки «особых случаев», которые по мнению автора не нужны.

          Ну и чем приведенный вами там код отличается от банального switch?


          PS Вы бы хоть Option[bool] взяли, для семантики "точно есть, точно нет, хрен знает".


          1. pawlo16
            18.10.2016 10:50

            Вот внесли вы его в «особые случаи» (т.е., кейсы в case class), и что дальше случилось со всем кодом, который использовал этот кейс-класс?

            Если код написан как у меня с дефолтным кэйсом
            | _ ->
            
            , то вообще ничего. Иначе компилятор выдаст варнинг, что в этом месте данный кэйс не учтён.
            Ну и чем приведенный вами там код отличается от банального switch?

            нет исключений, не надо явно прописывать все варианты, легко сопровождаем, можно масштабировать. например, можно заменить
            | CSharp 
            

            на
            | CSharp of Version * Platform
            
            без тяжёлой атлетики


            1. lair
              18.10.2016 10:55

              Если код написан как у меня с дефолтным кэйсом, то вообще ничего. Иначе компилятор выдаст варнинг, что в этом месте данный кэйс не учтён.

              Поведение с дефолтным кейсом ровно такое же, как и у switch. Поведение без него — такое же, как у switch с хорошим статическим анализатором.


              нет исключений, не надо явно прописывать все варианты

              В switch с default тоже нет исключений и не надо явно прописывать все варианты.


              легко сопровождаем

              В чем именно эта легкость выражается? Только во "вложенных данных"?


  1. Pilgrim_Nova
    19.10.2016 00:20
    +1

    1. На мой взгляд пример, вокруг которого строится повествование, несколько надуман.
    Именно в этом примере проблема кроется вовсе не в самой сущности перечислений, а в неверном их употреблении. God-объект возник вовсе не из-за того, что было использовано перечисление, а из-за того, что нарушен SRP. Никто не мешал поместить перечисление в корне компановки и инкапсулировать логику, подходящую конкретным языкам в отдельные сущности. Использовать switch с перечислением так же вовсе не обязательно. Ситуацию удобнее разрулить словарем — пожалуйста.

    2. Если ставить вопрос о необходимости предоставления возможности подключать к готовому продукту новые языки или разрабатывать их в параллель. То связь Язык — Реализация должна перекочевать в конфигурацию. И естественным образом никаких перечислений не будет, т.к. изначально из постановки ясно, что набор поддерживаемых языков расширяем. Логично, что это только в том случае, если и язык, и реализация могут быть предоставлены извне.

    3.

    Выбрасывать исключение в конце вынуждает компилятор, которому нужно обеспечить гарантированный результат каждой функции

    Компилятор не вынуждает выбрасывать исключения. Вынуждает логика — если мы не можем обработать неизвестный элемент без ошибки, то не важно, строка это или перечисление или что-то иное.
    Исключением мы лишь выражаем тот факт, что реализация не допускает произвольных значений, что если появится новый элемент, то его обработку нужно реализовать.
    В примере с иконками можно было вернуть специальную иконку для неподдерживаемого языка в default.
    Т.е. проблема опять же не в перечислении, а в том, что на момент реализации мы можем не иметь возможности предусмотреть поведение по умолчанию. И отказ от перечисления нас не спасет с учетом корректной реализации (п.1).

    4.
    Между TransactSQL и Java вообще может не быть ничего общего кроме того, что кто-то захотел открыть их в одном текстовом редакторе

    И этого вполне достаточно для их объединения на соответствующем уровне абстракции. Например, в списке поддерживаемых языков. Проблема из п.2 — перечисление просто не подходит для этой сущности.
    Но не в самом перечислении проблема, как языковой конструкции. Аналогичная картина с предложением использовать интерфейс: Выясняется, что сущность Язык более сложная в контексте использования в коде, естественно, что перечисление не подходит для ее отражения.

    Для чего я считаю нормальным использовать перечисления.
    Предположим, что у нас есть конченый на момент реализации список состояний сущности БД.
    Например, On и Off.

    При чтении из БД я преобразую эти значения в соответствующее перечисление:
    public enum SomeState
    {
    On,
    Off
    }

    Что я получаю: Методы, использующие эти состояние будут строго типизированы.
    Например:
    public interface IStateHandlerFactory
    {
    IStateHandler Create(SomeState state);
    }

    Если не использовать перечисления, то какие у нас есть варианты? Использовать само значение из БД. Предположим, что мы храним строки «On», «Off».
    Что получаем:
    public interface ISomeStateHandlerFactory
    {
    IStateHandler Create(string state);
    }

    С точки зрения клиента этого интерфейса может быть совершенно непонятно, какое значение туда можно передать, откуда его взять. Если у сущности несколько «состояний»?
    Так же, не посмотрев в БД, сложно определить все возможные значения.
    Как обрабатывать строку, по которой будет работать фабрика? Только сравнивая с самой строкой — а это уже дублирование. Как от него уйти — статический класс с константными строками, ничего не напоминает? Не наше ли это перечисление?

    Или еще проще. Предположим, в БД мы включаем/выключаем функционал.
    И в коде надо написать:
    if (entity.State == State.Off) return;

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