Комментарии 49
Вроде первоначальный вариант был прекрасен. Зачем вы всё испортили и обмазали всё паттерном?)
Потому что нельзя так просто написать "void main(){}".
Это не модно За такое не платят.
Иногда мне кажется, что те люди, которые пытаются так извращаться вместо простого цикла или условного оператора, на самом деле не любят программирование
Продолжайте использовать if-else! Конструкция настолько великолепна, что большинство языков программирования имеют ее в базовой библиотеке!
tldr: используйте для состояний классы, а не пеленки if-else
здесь закодирована кака-то бизнес-логика, вложенные условия, кривое форматирование, жызнь боль
При взгляде на простыню без if но с классами:
что тут воще происходит?
Не углубляясь в особенности Java или частную реализацию условий на машине состояний, сам посыл несмотря на провокацию, имеет толику смысла.
Технически, каждый if — это +1 к сложности кода. Одна функция с 4 условиями многократно сложнее для поддержки и понимания, чем 4 функции каждая с 1 условием.
Вдобавок, условие внутри именованной сущности как класс или функция, немедленно говорит о некоторых нарушениях принципа единой ответственности.
Важная ремарка: критически относиться следует к условиям которые меняют логику сущности, а не её внутренней реализации.
Использование условий следует изолировать в отдельную именованную сущность для сохранения сложности кода, вариации поведения выделять в родственные сущности за общим интерфейсом, который определен вызывающим кодом.
// Плохой пример. Избегайте условий глубоко в бизнес-логике.
void do_action(bool isWrite ){
if(isWrite){
do_read();
} else {
do_write()
}
}
/////////////
// Допустимо использовать условия, как часть внутренней реализации, но не рекомендуется. Имеет смысл смотреть в функциональные парадигмы и подходы.
int do_search( std::vector<int> collection ){
for(auto i : collection){
if( is_value_good_for_you(i) ){
return i;
}
}
}
/////////////
// Предпочтительное использование условий, вне бизнес-логики, для предварительной настройки взаимосвязей между сущностями.
handler_t* do_handler_configuration( config_t config ) {
if(config.use_A){
return new a_handler_t();
}
return new general_handler_t();
}
Возьмите на старте любой проект, для него идеально куча if-ов в бизнес логике, так как непонятно что делать и как, да и постоянно какие-то задачи уровня вкрутить акцию для новых пользователей, сделать интеграцию с этими (ой не подошли, давай с другими), собрать какую-то статистику, и прочее. В куче if проще разобраться, чем помнить все связи классов и как они менялись за последнее время. Важна гибкость.
Когда же проект устоялся, то да тут надо рефакторить и выносить части в классы, модули и накручивать другие абстракции. Тут уже важна поддерживаемость.
Согласен, здравый смысл в первую очередь.
Но кучей if-ы становятся уже после трёх в одной функции, потому что цикломатическая сложность приближается к 10. А бизнес-логика меняется так часто и неопределенно, что где 3 условия — там и 4, 5, 6, а потом еще и с force-флагом откуда-нибудь из глобальной области видимости.
Оверинжиниринг на 100500 утилитных классов связанных на внешнем слое — не совсем правильный подход к запуску нового продукта на неизведанном рынке. Но и намеренно макаронить код, оправдывая "Ща зарелизим, потом рефакторинг" — не выход. Как показывает практика — выделенный этап рефакторинга для устранения макарон из условий очень тяжело продать в продакт менеджмент, ибо новых фич не обещает, а гипотетическая поддерживаемость через год менее важна, чем 100 новых фич, каждая со своим условием, чтобы обратно совместимо.
И ростки плохой архитектуры начинают загнивать. А ростки хорошей — всегда можно вырастить в нужную сторону.
Менеждменту продать разработку супер архитектуры тоже тяжело — потому что дохода нет, а деньги теряем каждый день. А потом при кардинальном изменении бизнес требований опять все сначала в плане бумажной работы и рефакторинг.
Я не против хорошей архитектуры, я против преждевременной оптимизации архитектуры.
Целиком и полностью разделяю ваше мнение. Преждевременная оптимизация — время и деньги на ветер.
Я не видел еще ни одного проекта, где микросервисы уменьшают сложность. Обычно, как раз, бывает ровно наоборот, т.к. распределенные системы априори более сложные. Кроме того, обычно если команда не может писать нормальный код в монолите, то и в микросервисах выйдет не лучше.
Микросервисы — ни разу не панацея и в большинстве случаев приведет только к замедлению разработки и усложнению поддержки.
Микросервисы уменьшают сложность путём чёткого очерчивания границ модулей, резко увеличивая сложность их нарушения. В монолите нам, например, обычно не составит труда к запросу на получение юзера из таблицы аутентификационных данных подтянуть ещё и данные о непрочитанных сообщениях. Но запрос получится сложным, можно и налажать легко, например inner join использовать вместо left join и получить что пароль невереный, хотя просто нет непрочитанных сообщений. А в микросервисах это будет два простых запроса, налажать в которых значительно сложнее.
Кроме того, в реальности же часто получается так, что в погоне за «четкими границами модулей», эти границы очерчиваются так, что о независимых релизах люди вспоминают только в мечтах.
Можно говорить о том, что должны существовать умные дядьки, правильно ограничивающие границы и т.п. Но в Java это уже все проходили, причем неоднократно — куча идей, проектов, стандартов и мечтаний о том, что архитекторы нарисуют диаграммы классов, а джуны закодят. И все это выливается все равно в монструозные и неподдерживаемые системы.
Безусловно, существуют проекты, где микросервисы будут во благо, но таких проектов очень мало. Намного меньше, чем большинству кажется.
Это если язык поддерживает модули более-менее, то ещё можно как-то. А если вся надежда только на код-ревью, то разделить процессы надёжней. Ну и продать бизнесу идею перехода на микросервисы может оказаться проще чем идею "а давайте всё перепишем, а то границы модулей нарушены". А так "под шумок" и те же CI/CD, и автотесты, и тулзы можно внедрить. В общем каша из топора :)
В общем я к чему — микросервисы не серебряная пуля, но кроме основных своих преимуществ (независимый деплой, масштабирование и т. п.) они могут нести другие плюсы, которые можно получить и другими способами, а можно и микросервисами.
уже после трёх в одной функции, потому что цикломатическая сложность приближается к 10
Эм, цикломатическая сложность цепочки вызовов из 3 виртуальных функций, каждая из которых на 2 класса, тоже приближается к 10.
'if (cond) else'
можно заменить на 'jmp if_table[(int)cond]'
, что по цикломатической сложности ничем не отличается от вызова виртуальной функции 'call object.virtual_table[function_index]'
.
Я ещё понимаю отказ от else if
в сторону возврата значения в функции или свитча, но вариант который предлагаете вы я не могу сходу понять. Результат — цель не достигнута. Стало хуже
function shmunction(data) {
if(notValid(data)) return false
if(specialData(data)) return transformed(data)
return data
}
Что только не придумают когда нету сум-типов.
А зачем тут сум-типы? Конкретно в этой задаче достаточно перечисления...
Потому что внутренние поля при разных состоянихя разные. Типа CancelationReason в одном случае и TransactionId в другом. И даже если одинаковые внутренее данные получаем больше на уровне типов, т.к. функция PrepareRoom должна принмать только AcceptedBooking а не BaseBooking внутри которого enum BookingState.
Может я чуть отклонился от терминологии в посте, но мысль надеюсь понятна.
Nicklas Millard works at one of the Big4 Consulting companies in Denmark as a Senior Technology Consultant. He’s primarily taking the role as lead developer and solution architect on client projects.
He’s been developing software for commercial clients and government institutions such as the Ministry of Defence, Ministry of Education, Ministry of Environment and Food of Denmark, The National Police, Danish Agency for Labour Market and Recruitment, and Ørstad.
Я понял направление статьи сам некоторые вещи решаю так, но делаю немного иначе так как ваши примеры нарушают некоторые принципы SOLID + «Просто добавить новый класс» в такой реализации не выйдет. Почему не выйдет? Что Вы будете делать когда в каком-то состоянии 2 из 3 методов реализовать просто нельзя? NotImplementedException? null — нарушаем LSP.
Что Вы будете делать когда в каком-то состоянии 2 из 3 методов должны иметь ту же реализацию что и в другом стейте?
Я рекомендую сильнее дробить реализации этих паттенов, то-есть брать не целый Агрегат и описывать его состояния такими классами, а реализовывать стратегию\состояние с одним методом (Handle, Execute, Run, MoveNext, etc). В случае «Стратегии» — просто что-то делаем, «Состояние» — что то делаем и возвращаем новое состояние.
При таком дроблении у вас будет на каждый метод (Enter, Cancel, Accept) свои пачки разных реализаций, где-то 2, где-то 3, где-то 5, а какой-то класс выше уже будет всем этим рулить. Хотя и тут уже не все тривиально выглядит :), ведь все равно кто то будет через if-else выбирать реализацию
Updated:
Жалко что статья — перевод, автор не обсудит мои мысли со мной
Стратегия вообще универсальная и используется по факту и в «команде» и в «итераторе» (частный случай — алгоритм перебора коллекции)
Но в остальном согласен.
Ну то есть все с if-else нормально, но состояние — хороший способ их декомпозировать.
Просто тут примеры выглядят неудачными
Жалко что статья — перевод, автор не обсудит мои мысли со мной
Надо привлекать англоязычную аудиторию на хабр. И обсуждать с ними на инглыше.
Потому что пока там тишина и мертвые с косами стоят :)
Да там нормально комментов, чего: https://medium.com/p/f4d2323e6e4/responses/show
И самые заплюсованные, конечно, "нет, спасибо".
ЗЫ:
Привлекать аудиторию, конечно, надо, но перетягивать из оригинала — нелепо.
Там есть возможность комментирования и связи с автором — хочется что-то сказать — значит надо идти туда.
Перетягивать надо оригинальными (перевод русскоязычной — тоже оригинал) статьями на английском, что многие тут уже делают.
Вот если б кто написал ответную статью, кто-то перевёл её — тогда можно было бы скинуть автору, мол, приходи, комментируй к нам.
Примерная расшифровка треда в ответе на первый заплюсованный комментарий
К: комментатор, А: автор
К: Заголовок желтоват, и делать заключение, что if..else плохое заменяя не очень распространённым подходом — неправильно
-
А: Заголовок может и желтоват, но это действительно моё мнение, я не использую конструкцию if..else в проде уже два года
-
К: Серьёзно? Два года? Да ты не программируешь вообще!
-
А: Не, ну, я пишу тыщи IFов. Но if..else? Не очень много. Конструкция if..else редко необходима, часто это признак плохо структурированного и плохо расширяемого кода.
-
Такие дела. Из чего у меня напрашивается самый очевидный вывод — автор предпочитает early-return, предложенный (и заминусованный) даже тут где-то в комментариях.
ЗЫ: Ну и странный же тут MD форматтер.
С самими if — else все нормально, их можно и нужно использовать.
Просто нужно понимать ограничения и сложности.
Но тут есть несколько разных проблем — большое количество условий и дублирование условных операторов на разных уровнях.
Первая проблема — большие каскады else if плохо читаемы. Их нужно просто декомпозировать по правильным модулям и сводить к плоским случаям в духе «либо выполнил операцию, либо вернул false ранним выходом». Ну и про нейминг не забывать.
Вторая проблема — множественные проверки, связанные с flow.
Их характерная особенность — когда в нескольких методах встречаются похожие наборы ифов или даже switch.
В этом случае да — речь скорее всего идет о том, что нужно использовать полиморфизм и состояния.
В остальных случаях все с ифами нормально. А если у вас нет хороших идей по организации кода — даже свич на 1000 строк неплохой архитектурный паттерн.
Исходный код трудночитаемый, но предлагаемое решение выглядит еще хуже.
Я в таких случаях использую таблицы решений (decision tables). Это наглядный метод табличного описания алгоритмов, который описываются большим количеством логических операторов if и switch.
Я ними познакомился в 2000 году во время работы над проектом, который реализовывался по методологии Хэтли-Пирбхаи. С тех пор я регулярно использую их как для написания нового кода, так и для анализа существующего кода.
Это очень давно известная методика. Хорошее описание можно найти в книге издательства "Мир" 1976 года: Э.Хамби "Программирование таблиц решений".
Методология Хэтли-Пирбхаи, одной из составных частей которых является использование таблиц решений, описана в книге Hatley & Pirbhai "Strategies for Real Time System Specification" https://www.amazon.co.uk/gp/product/0932633110 Есть более свежее издание: https://www.amazon.co.uk/gp/product/0932633412
Я искренне честно прочитал эту статью два раза, чтобы исключить свою предвзятость. Рассуждения автора мягко говоря перекликаются с книгой Elegant Objects. Возможно такие «паттерны» берутся от слабого понимания промышленного кода и требований к нему. Ведь ни один не приводит примеры такого реально работающего кода в больших проектах на миллион (или в этом случае «на миллиард») строчек. У автора прекрасно получилось обмазать простой до глухого щелчка код абстракциями и паттернами, растянуть его примерно в 10 раз. Вы поймите меня правильно, этот высосанный непонятно из какого места пример очень пахнет оверхедом, вот и все, это такой же «запах» в коде как дублирование или глупые комментарии.
Порой я думаю, что такие статьи пишутся ради статьи.
for(int i=0;i<10;i+=1){
if(condition1){do_stuff_1;}
if(condition2){do_stuff_2;}
if(condition3){do_stuff_3;}
}
не проверяются условия condition1 и condition2, а когда я убираю условие condition3, то эти условия проверяются.
Поставил в первые два условия булевую переменную bool
for(int i=0;i<10;i+=1){
if(condition1){do_stuff_1; bool=true;}
if(condition2){do_stuff_2; bool=true;}
if(condition3 && !bool){do_stuff_3;}
}
В итоге третье условие не выполняется и программа вроде работает корректно, хотя и выглядит это не очень.
Звучит как бред. Скорее всего всё у вас проверяется, просто блок do_stuff_3 отменяет то, что делалось в do_stuff_1 и do_stuff_2.
Возможно, эти условия достаточно поменять местами. А может, как раз нужен критикуемый тут if-else. Впрочем, ваш вариант с флагом тоже нормальный, если только не называть переменную bool.
просто блок do_stuff_3 отменяет то, что делалось в do_stuff_1 и do_stuff_2
Да, действительно.
В итоге у меня
if(condition1 && !bool){do_stuff_1; bool=true;}
if(condition2 && !bool){do_stuff_2; bool=true;}
if(condition3 && !bool){do_stuff_3; bool=true;}
if(condition4 && !bool){do_stuff_4; bool=true;}
но использование флага выглядит как костыль.
Может можно, как в статье, создать класс с двумя методами Accept и Cancel
(далее создать два отдельных класса для двух состояний)?
Самое время начать использовать цепочки if — else if — else if...
Кажется, только меня одного смутило то, насколько бездарно переведена статья. Я тут недавно, может, тут не принято критиковать форму. Но только сама подача материала мотивирует не читать дальше, но продолжать использовать if-else.
Извините меня за грубость, конечно, но это онанизм.
Это сроде малолеткам, которые только начали осваивать язык, и пытаются всячески выпендриться перед своими сверстниками, заявляя: "А смотрите, как Я МОГУ!"
Конечные автоматы?
Не, не слышали!
Плодим новые сущности без сильной надобности?
Ну и фиг с ним!
Код будет выполняться медленнее?
Да кого это заботит!
Отладка всего этого дела превращается в ад?
Да ты просто вот эту крутую IDE не осилил, кекеке!
Отвратительно до боли.
Приведенный пример if-else ужасен, идея с состояниями имеет право на жизнь но реализация здесь точно так же ужасна
Прекратите использовать оператор If-else