Pull to refresh

Comments 49

Вроде первоначальный вариант был прекрасен. Зачем вы всё испортили и обмазали всё паттерном?)

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

Продолжайте использовать if-else! Конструкция настолько великолепна, что большинство языков программирования имеют ее в базовой библиотеке!

Я ещё слышал что-то насчёт goto…
Простите, я один вижу что стало хуже?
Автор похоже узнал про существование FSM. Когда автор узнает про map/reduce будет новая статья про отказ от циклов :)
кажется уже было…
А концепция, на которой работает SQL, просто повергнет его в экстаз.
Вариант на if-else выглядит наиболее понятным

tldr: используйте для состояний классы, а не пеленки if-else

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

Это все делается прекрасно и в монолите — просто поделите на модули. А чтобы джунам было неповадно нарушать границы, есть code review и всякие автоматизированные тула аля JArchitect / ArchUnit (для Java). Если же нет нормального code review и статического анализа, то и микросервисы выйдут такими, что поддерживать это будет нереально.

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

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

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

Это если язык поддерживает модули более-менее, то ещё можно как-то. А если вся надежда только на код-ревью, то разделить процессы надёжней. Ну и продать бизнесу идею перехода на микросервисы может оказаться проще чем идею "а давайте всё перепишем, а то границы модулей нарушены". А так "под шумок" и те же CI/CD, и автотесты, и тулзы можно внедрить. В общем каша из топора :)


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

уже после трёх в одной функции, потому что цикломатическая сложность приближается к 10

Эм, цикломатическая сложность цепочки вызовов из 3 виртуальных функций, каждая из которых на 2 класса, тоже приближается к 10.
'if (cond) else' можно заменить на 'jmp if_table[(int)cond]', что по цикломатической сложности ничем не отличается от вызова виртуальной функции 'call object.virtual_table[function_index]'.

Простите, а где вы Java увидели? Ни в статье, ни в вашем примере кода ее нет.

Я ещё понимаю отказ от else if в сторону возврата значения в функции или свитча, но вариант который предлагаете вы я не могу сходу понять. Результат — цель не достигнута. Стало хуже


Пример функции без 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.
Вы совместили 2 паттерна «Стратегия» и «Состояние», и получилось это не лучшим способом.
Я понял направление статьи сам некоторые вещи решаю так, но делаю немного иначе так как ваши примеры нарушают некоторые принципы 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 строк неплохой архитектурный паттерн.
Первым был if-then-else. Сейчас конечные автоматы. Ждём третий этап — схемы и движки BPM. :)

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


Я в таких случаях использую таблицы решений (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...

Ладно, попробую)). Просто хотелось разобраться в коде из статьи, но на более простом примере. Можно ли создать класс, например, myBool. Тогда проверка условий должна, наверное, выполняться методами класса, т.е.
myBool.accept
myBool.cancel

Вы пытаетесь использовать ООП для решения несуществующей проблемы.


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

Кто-то прочитал книжку про паттерны и теперь не знает куда же ещё можно их запихать.

Кажется, только меня одного смутило то, насколько бездарно переведена статья. Я тут недавно, может, тут не принято критиковать форму. Но только сама подача материала мотивирует не читать дальше, но продолжать использовать if-else.

Извините меня за грубость, конечно, но это онанизм.
Это сроде малолеткам, которые только начали осваивать язык, и пытаются всячески выпендриться перед своими сверстниками, заявляя: "А смотрите, как Я МОГУ!"


  • Конечные автоматы?


  • Не, не слышали!


  • Плодим новые сущности без сильной надобности?


  • Ну и фиг с ним!


  • Код будет выполняться медленнее?


  • Да кого это заботит!


  • Отладка всего этого дела превращается в ад?


  • Да ты просто вот эту крутую IDE не осилил, кекеке!



Отвратительно до боли.

Боюсь показаться очевидным, но когда нужен if-else нужно использовать if-else, а когда нужны состояния нужно использовать состояния, не нужно использовать одно вместо другого.

Приведенный пример if-else ужасен, идея с состояниями имеет право на жизнь но реализация здесь точно так же ужасна
Sign up to leave a comment.