Comments 70
Выбрасывать исключение в конце вынуждает компилятор, которому нужно обеспечить гарантированный результат каждой функции, а проконтролировать полноту покрытия множества значений оператором switch компилятор C# не может.Лечится плагином-анализатором к рослину. Зачем из этого целую проблему раздувать и обзывать антипаттерном — не ясно.
Это "объектная" ориентация. Все в этом мире — объект. Потом чешут репу и удивляются обилию аллокаций и GC.
Хотя мне кажется тут автор пропустил один важный компонент — не хватает абстрактной фабрики провайдеров языков. Ну чтобы уж точно добить бедный простой switch-case. А то как-то предложенное решение не до конца "объектно" ориентированно.
Остальные проблемы являются либо надумаными, либо связаны с неумением мыслить в функциональном стиле и писать/поддерживать функциональный код.
Чтобы не использовать enum, заведите интерфейс. Интерфейс должен возвращать информацию о свойствах объекта из описываемого множества. [...] Теперь все функции, содержащие switch, если они у вас были, удалите. Они вам больше не понадобятся, потому что код обрабатывает не конкретные объекты, а их свойства.
Круто. Теперь представьте себе, что у меня есть внешний по отношению к этому интерфейсу код (т.е., интерфейс и его реализации лежат в библиотеке, а мой код просто эту библиотеку потребляет), и мне надо расширить поведение — например, мне надо в зависимости от пары языков предоставить конвертер из одного языка в другой. Ваши предложения?
Не говоря уже о том, что enum — это всего лишь формализованное конечное множество, а преобразование из одного множества в другое — это одна из базовых операций в функциональном программировании, и что в ней плохого, спрашивается?
состояния конечного автомата
Адвокат дьявола просит напомнить вам про паттерн State.
P.S.
проконтролировать полноту покрытия множества значений оператором switch компилятор C# не может.
Почему не может-то?
Во-первых, в статье нет ограничения, что этот подход применим только к бизнес-логике.
Во-вторых, а почему нет? Даже оставаясь в рамках примера из статьи, можно легко увидеть такой сценарий: предположим, что у "редактора" плагинная структура, интерфейсы определены в ядре, каждый язык — плагин, и вся дополнительная функциональность — тоже плагин. Соответственно, если я хочу сделать плагин с конвертерами из языка в язык, то для меня и интерфейсы будут в библиотеке (ядре), и языки будут в библиотеках (плагинах), и совершенно не обязательно они будут мне подконтрольны. И это далеко не единственный сценарий.
Чем этот filetype отличается от перечисления (кроме того, что он строковый)? Как я могу применить к нему описанную в статье методику "вытащите интерфейс"?
«Вытащите в интерфейс» методика уже применена.
а filetype это свойство языка и обрабатывается оно через полиморфизм.
Вы под "свойством языка" понимаете свойство (property) объекта, описывающего язык, который поддерживается редактором? А какого это свойство типа?
При замене одного языка на другой, подгружается одноименный файл-обработчик со всей логикой.
Вот только у меня не замена одного языка на другой. Мне надо реализовать код, который в зависимости от двух выбранных языков ("из" и "в") будет вызывать ту или иную конверсию.
«Вытащите в интерфейс» методика уже применена.
В коде редактора, но не в коде моего плагина.
Мне надо реализовать код, который в зависимости от двух выбранных языков («из» и «в») будет вызывать ту или иную конверсию
Ну это же просто, ваш плагин должен реализовать некую функцию вида convert(langA, langB) с передачей ему имен целевых языков (или их объектов). Чтоб было понятнее, я приведу другой пример из реальной практики. Я реализовывал плагин для работы с системами deploy в Vim. Разумеется онных десятки, потому я сначала реализовал абстрактный плагин vim_deploy, описывающий семантику всех систем деплоя, а затем для каждой конкретной системы реализовывал конкретный адаптер в контексте описанной семантики. Если адаптер-плагину необходимо будет расширить семантику системы деплоя, он просто реализует новые методы, но использовать их полиморфно уже не получиться. Если сравнивать это решение с enum, то последний имеет ряд недостатков, основным из которых является применение перечисления для представления бесконечного множества.
Ну это же просто, ваш плагин должен реализовать некую функцию вида convert(langA, langB) с передачей ему имен целевых языков (или их объектов).
Именно. Как внутри этой функции мне выбрать нужное преобразование?
Если семантика объекта-языка позволяет, то можно использовать полиморфизм
Полиморфизм — это хорошее красивое слово, но можно конкретный пример?
иначе лучше использовать некий объект-конвертер, как это реализовано в C++ с перегрузкой операторов (логика преобразования из языка langA в язык langB заложена в объекте класса langA или langB).
Напомню, что у меня нет контроля ни за реализацией langA
, ни за реализацией langB
.
Вы реализуете класс 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 тестирование" я точно вступать не хочу.
Практически вы всегда имеете дело с конечными множествами
Не совсем, множество языков бесконечно в том смысле, что это создает дополнительную связь нисколько не решая проблемы выбора конвертера.
Ослабив связь, вы ухудшили валидируемость решения
Почему? На мой взгляд валидируемость решения не пострадает если есть тесты. Я привык валидировать решения тестами, а не статическим анализатором (но не считаю, что последнее решение бесполезно или вредно).
Ключевое слово — «если»
Ну без «если» никуда )
Зависит от того, что язык поддерживает.
Я, в среднем, считаю, что перечисления неплохи, а типы-суммы удобнее перечислений (но имеют все те же проблемы поддерживаемости). С type classes я не работал, поэтому ничего не могу сказать.
При этом можно построить объектное решение, но в нем неизбежно рано или поздно окажется дискриминатор, который все равно вернет решение к перечислениям и их аналогам.
Почему не может-то?
Я не знаю. Мне, зашедшему в гости из Java, это непонятно. Возможно, мои друзья из мира C# не все мне рассказали.
например, мне надо в зависимости от пары языков предоставить конвертер из одного языка в другой. Ваши предложения?
Это типичный double dispatch. В классических ооп-языках со статической типизацией — решается исключительно через
Только причем тут enum и switch? Они проблему не решают никак.
Это типичный double dispatch. В классических ооп-языках со статической типизацией — решается исключительно через жовизитор.
Для этого надо, чтобы оригинальный интерфейс это поддерживал.
Только причем тут enum и switch? Они проблему не решают никак.
Прекрасно решают: заводим enum по языкам, затем в switch отвечаем, поддерживаем ли такой конвертер.
Для этого надо, чтобы оригинальный интерфейс это поддерживал.
Accept вполне себе прицепляется extension method'ом или враппером-аксептором.
Прекрасно решают: заводим enum по языкам, затем в switch отвечаем, поддерживаем ли такой конвертер.
Это ровно тот же самый кривой double dispatch, только процедурный.
Не то чтобы я настаивал на ООП-подходе, но проблема вообще нормально не решается без нормального мультидиспатча. Аксиома Эскобара как она есть.
В некоторых языках программирования, например в Haxe, использование связки enum-switch считается рекомендованным подходом http://haxe.org/manual/types-enum-using.html И про это пишут в документации
Все, что вы здесь сделали, называется простым словом "рефакторинг", и прием, описанный Фаулером — "экстрагирование классов". Если перечисления нужны именно как перечисления, без подключения тонны дополнительной логики, пусть они так и останутся перечислениями. Для более сложных случаев подойдут другие решения, например, ваше.
Мне кажется, или идея менять enum на interface написана где-то на перых полутора страницах книжки Design Patterns?
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 сам про себя умеет отвечать всё, что нужно — то, собственно, почему бы и нет?
бить «палкой по рукам» людей, которые будут напрямую на варинты enum'а ссыслаться вне его
Надо еще отыскать людей, которые делают сие непотребство. А для этого вам нужен внешний инструмент. То есть, либо настроить проверку R#, либо что-то вроде PVS, либо свой анализатор. И я не уверен, что первые два работают такое в билд процессе.
Кстати, те же анализаторы эффективнее Style Guid'ов: машина отлавливает ошибки стабильнее ревьюеров (и затрат сил меньше).
Довольно очевидно, что enum из дней недели и через 100 лет будет 7 членов содержать, а вот список поддерживаемых языков — может меняться.
Если я правильно вас понял, вы предлагаете ему открыть StyleGuide, найти там страничку с перечислениями, и оттуда узнать, перебираемый этот enum или нет. Вы не находите, что это несколько неудобно?
Но даже если мы вынесли информацию о переборности в комментарии\атрибуты — программисту надо чекнуть enum, ревьюеру надо чекнуть enum. Это снова неудобно, особенно ревьюеру, а значит вполне возможна ситуация «Наверняка это перебирается» + «Раз Вася поставил — значит можно». В итоге мы имеем баг, который через годик прострелит кому-то колено. И виновникам бага палкой по рукам не настучать — они уже в другой конторе (хотя настучать очень хочется).
То есть, разделение enum по «перебираемое»\«неперебираемое» без автоконтроля не защищает от ошибок, да и автоконтроль еще надо настроить. Хотя варианта лучше\проще все равно нет.
Это убирает главный минус анти-паттерна switch — необходимость искать по всему коду.
Однако необходимость вносить изменения во все ветки остается.
При этом если в одном языке появится состояние(например версия для определения списка ключевых слов), то придется передавать все состояния все поддерживаемых языков в эти методы.
И базового интерфейса есть другой недостаток: при расширении функциональности (те же ключевые слова), придется вносить изменения в различных местах, не все из которых контролируются (плагины)
ПС. Если планируется расширение перечисления, то это очень серьезный кандидат на выделение интерфейса. Пока switch'и не расползлись по коду.
https://habrahabr.ru/post/112123/
только вместо C# — C++
У любой проблемы (а лишний слой абстракции — это проблема всегда, хотя иногда и вынужденная) должно быть имя.
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 подставляются логин и пароль) и метод запроса и отправляет всё это добро на вебсервис.
Смущающий меня момент как раз таки в километровом свиче, который обрабатывает ответ в зависимости от запроса (а точнее, в зависимости группы, в которую я отнес этот запрос). И не спасает даже параметр группировки, который я ввел в элементы перечисления для однотипных ответов.
Можно ли как-то облагородить схему?
Создайте 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);
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
? А смысл тогда первые три значения вводить?
смотря что проще — внести его в конфиг, либо рассматривать как «особый случай»
== смысл тогда первые три значения вводить?
для упрощения обработки «особых случаев», которые по мнению автора не нужны. В предметной области описания ЯП их немерено.
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
тоже нет исключений и не надо явно прописывать все варианты.
легко сопровождаем
В чем именно эта легкость выражается? Только во "вложенных данных"?
Именно в этом примере проблема кроется вовсе не в самой сущности перечислений, а в неверном их употреблении. 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;
Помимо этого, стоит принять во внимание то, что строки — ссылочный тип, а перечисление — структура.
И это не говоря, о каких-то перечислениях, используемых только внутри какой-то абстракции и не выходящих наружу.
Enum-switch антипаттерн