Pull to refresh

Comments 70

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

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

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

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

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


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


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

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


P.S.


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

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

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

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


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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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


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

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

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

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


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

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


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

Как мне выбрать нужный 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)
}
}


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

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


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

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


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

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


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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

Ну без «если» никуда )
UFO just landed and posted this here

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


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


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

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


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

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

Чтобы получить поддержку профессионального сообщества и больше узнать по интересующей теме.
UFO just landed and posted this here
Класс типов там, где достаточно простой алгебры — как это по-хаскельски! Сделать всё максимально сложно!
например, мне надо в зависимости от пары языков предоставить конвертер из одного языка в другой. Ваши предложения?

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

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


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

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

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


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

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


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

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

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

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

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

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

UFO just landed and posted this here

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

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

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

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

Как и любое повторяющееся логическое ветвление, описанный случай со switch — классический запашок кода, где пора использовать полиморфизм.
Пример 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 блоки). Сложности не добавит, но информация будет чуть более полной.
Пардон, пропустил в тексте.
UFO just landed and posted this here
Я сделать как «у порядочных сволочей» нельзя? В смысле — засунуть все эти методы в сам enum и бить «палкой по рукам» людей, которые будут напрямую на варинты enum'а ссыслаться вне его? В C# синтаксис, конечно, не так удобен, как в Java, но и не то, чтобы уж прямо ужасен.

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


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

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

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

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

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

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

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

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

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

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

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

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

type Language with
    // F# - наличие провайдеров типа 
    static member hasTypeProviders = function
        | FSharp -> true
        | _ -> false

    // SQL, Clojure - наличие транзакций
    static member hasTransactions = function
        | TSQL | MySQL | Clojure -> true
        | _ -> false

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

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


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

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


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

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

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

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

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

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

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


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

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


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

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

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;

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

Articles