Enum-switch антипаттерн

    В последнее время мне часто встречается в коде один интересный шаблон. Он заключается в том, что для описания небольшого множества объектов создается 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}
    • дни недели, месяца
    • состояния конечного автомата
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 70

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

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

          0
          Гораздо более удивляет, когда добавление какой-нибудь безобидной штуки, в которой нет ничего нового, программу приходится полностью перерабатывать, долго тестировать, и все равно в продакшене лезут ошбики.
          0
          Не понимаю, как плагин может решить такие проблемы, как-то: появление нежелательных зависимостей, совместное редактирование общего кода, распухание функций.
            0
            Есть понятие закрытого множества объектов (discriminated union в F#, case class в Scala, variant в Nemerle). И вам надо гарантировать, что все элементы этого закрытого множества будут обработаны, чего компилятор C# из коробки не обеспечивает, но что можно к нему без всяких проблем добавить посредством анализатора.

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

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


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


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

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


          P.S.


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

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

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

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


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

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

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

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

                    «Вытащите в интерфейс» методика уже применена.
                      –1
                      а filetype это свойство языка и обрабатывается оно через полиморфизм.

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


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

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


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

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

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

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

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

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

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

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


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

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

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

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


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

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


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

                                    0
                                    Как мне выбрать нужный 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
                                      Ну если у вас есть имя целевого языка, то это не составит труда, к примеру можно использовать это имя в качестве имени конвертера.

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


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

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


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

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


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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

                                            Ну без «если» никуда )
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0

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


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


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

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


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

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

                    –2
                    Чтобы получить поддержку профессионального сообщества и больше узнать по интересующей теме.
                  +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, и если для бизнес-логики этого будет достаточно.
                    –3
                    Класс типов там, где достаточно простой алгебры — как это по-хаскельски! Сделать всё максимально сложно!
                    0
                    например, мне надо в зависимости от пары языков предоставить конвертер из одного языка в другой. Ваши предложения?

                    Это типичный double dispatch. В классических ооп-языках со статической типизацией — решается исключительно через жовизитор.
                    Только причем тут enum и switch? Они проблему не решают никак.
                      0
                      Это типичный double dispatch. В классических ооп-языках со статической типизацией — решается исключительно через жовизитор.

                      Для этого надо, чтобы оригинальный интерфейс это поддерживал.


                      Только причем тут enum и switch? Они проблему не решают никак.

                      Прекрасно решают: заводим enum по языкам, затем в switch отвечаем, поддерживаем ли такой конвертер.

                        0
                        Для этого надо, чтобы оригинальный интерфейс это поддерживал.


                        Accept вполне себе прицепляется extension method'ом или враппером-аксептором.

                        Прекрасно решают: заводим enum по языкам, затем в switch отвечаем, поддерживаем ли такой конвертер.


                        Это ровно тот же самый кривой double dispatch, только процедурный.

                        Не то чтобы я настаивал на ООП-подходе, но проблема вообще нормально не решается без нормального мультидиспатча. Аксиома Эскобара как она есть.
                          0
                          Accept вполне себе прицепляется extension method'ом или враппером-аксептором.

                          А зачем, если код со switch — проще?

                            0
                            Он проще ровно до момента пока свичей не станет два.
                              0

                              Визиторов в этот момент тоже станет два.

                    +2

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

                    • НЛО прилетело и опубликовало эту надпись здесь
                      +4

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

                        0
                        Согласен. Если нетрудно, дайте ссылку на Фаулера.
                          0
                          https://www.ozon.ru/context/detail/id/1308678/

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

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

                          0
                          Как и любое повторяющееся логическое ветвление, описанный случай со switch — классический запашок кода, где пора использовать полиморфизм.
                            0
                            Вам не кажется.
                            0
                            Пример 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;
                            }
                            

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


                            По статье же — в ней было бы уместно показать еще и проблемы с обновлением enum'a (добавил поле — обнови все switch блоки). Сложности не добавит, но информация будет чуть более полной.
                            Пардон, пропустил в тексте.
                            • НЛО прилетело и опубликовало эту надпись здесь
                                +1
                                Я сделать как «у порядочных сволочей» нельзя? В смысле — засунуть все эти методы в сам enum и бить «палкой по рукам» людей, которые будут напрямую на варинты enum'а ссыслаться вне его? В C# синтаксис, конечно, не так удобен, как в Java, но и не то, чтобы уж прямо ужасен.

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


                                  Надо еще отыскать людей, которые делают сие непотребство. А для этого вам нужен внешний инструмент. То есть, либо настроить проверку R#, либо что-то вроде PVS, либо свой анализатор. И я не уверен, что первые два работают такое в билд процессе.
                                    +1
                                    Если у вас в проекте есть люди, которые не соблюдают вещи, явно написанные в Style Guide'е, то вам никакой рефакторинг не поможет.
                                      0
                                      Вы предлагаете вообще запретить прямые ссылки на enum? Или вносить в Style Guide каждый enum по отдельности?
                                      Кстати, те же анализаторы эффективнее Style Guid'ов: машина отлавливает ошибки стабильнее ревьюеров (и затрат сил меньше).
                                        0
                                        Style Guide, в общем-то, не машина интерпретирует. Запретить делать «switch» для enum'ов, которые могут в будущем получить новые члены.

                                        Довольно очевидно, что enum из дней недели и через 100 лет будет 7 членов содержать, а вот список поддерживаемых языков — может меняться.
                                        • НЛО прилетело и опубликовало эту надпись здесь
                                            0
                                            Окей, у вас есть два десятка перечислений в решении. Для 10 из них switch разрешен, для 10 нет. Программист получает таск и понимает: есть значение AnyEnum, в зависимости от него надо выполнять разные действия.
                                            Если я правильно вас понял, вы предлагаете ему открыть StyleGuide, найти там страничку с перечислениями, и оттуда узнать, перебираемый этот enum или нет. Вы не находите, что это несколько неудобно?
                                            Но даже если мы вынесли информацию о переборности в комментарии\атрибуты — программисту надо чекнуть enum, ревьюеру надо чекнуть enum. Это снова неудобно, особенно ревьюеру, а значит вполне возможна ситуация «Наверняка это перебирается» + «Раз Вася поставил — значит можно». В итоге мы имеем баг, который через годик прострелит кому-то колено. И виновникам бага палкой по рукам не настучать — они уже в другой конторе (хотя настучать очень хочется).
                                            То есть, разделение enum по «перебираемое»\«неперебираемое» без автоконтроля не защищает от ошибок, да и автоконтроль еще надо настроить. Хотя варианта лучше\проще все равно нет.
                                      0
                                      Enum в Java сделан гораздо лучше, и там его область применения гораздо шире. Хотя все равно остается класс, который должен содержать в себе данные обо всех.
                                        0
                                        Ваш пример — это промежуточный вариант: все выборы сосредоточены в одном месте.
                                        Это убирает главный минус анти-паттерна switch — необходимость искать по всему коду.
                                        Однако необходимость вносить изменения во все ветки остается.
                                        При этом если в одном языке появится состояние(например версия для определения списка ключевых слов), то придется передавать все состояния все поддерживаемых языков в эти методы.
                                        И базового интерфейса есть другой недостаток: при расширении функциональности (те же ключевые слова), придется вносить изменения в различных местах, не все из которых контролируются (плагины)
                                        ПС. Если планируется расширение перечисления, то это очень серьезный кандидат на выделение интерфейса. Пока switch'и не расползлись по коду.
                                        0
                                        Ветвления. Что с ними можно сделать
                                        https://habrahabr.ru/post/112123/

                                        только вместо C# — C++
                                          0
                                          Спасибо.
                                          +1
                                          Со статьей, в целом, я согласен. Однако мой опыт отличается от Вашего, нынче наблюдаю обратный процесс, люди начитавшись книг по ООП(что само по себе и неплохо) начинают выделять интерфейсы и творить «энти фабрики, стратегии» там где это излишне, так ещё для большей расширяемости интерфейс выкидывают в отдельную либу, и мы имеем пятьсотмиллионов проектов). Если расширяемость в данном месте не предполагается, можно и со switch начать, а далее перескочить недолго(если конечно не доводить до портянок в десяток case, которые используются в десятке мест).
                                            0
                                            Это верно. Некоторые начинающие разработчики создают излишние слои абстракций, потому что считают, что это проявление профессионализма. На деле получается не надежный код, а имитация бурной деятельности. Это тоже большая проблема, заслуживающая отдельной статьи.
                                              +1
                                              Есть простой тест. Задёте себе вопрос: кто пострадает, если этой абстракции вот прямо тут не станет. Если ответ — «дядя Вася из соседнего отдела»… идёте говорить с Васей. Если ответ — «ну… кто-нибудь, когда-нибудь, в будущем, фиг знает когда»… выкорчёвываете абстракцию нафиг.

                                              У любой проблемы (а лишний слой абстракции — это проблема всегда, хотя иногда и вынужденная) должно быть имя.
                                            0
                                            Впервые работаю над приложением с постоянными веб запросами на каждый чих, после некоторых метаний остановился на варианте с перечислением для запросов (язык 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 подставляются логин и пароль) и метод запроса и отправляет всё это добро на вебсервис.

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

                                            Можно ли как-то облагородить схему?
                                              0
                                              Это код на 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);
                                              

                                              0
                                              Фреймворк, интерфейс, провайдер — унылое императивное ООП. Не надо этого. В 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 )
                                              
                                                0

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

                                                  +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 в данном случае, иначе этот тип рискует разрастись до абсурдных размеров.
                                                    0
                                                    смотря что проще — внести его в конфиг, либо рассматривать как «особый случай»

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


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

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


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

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

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

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

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

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

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


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

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


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

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

                                                +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;

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

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

                                                Самое читаемое