Pull to refresh

Comments 205

UFO just landed and posted this here
Рад, что вы оценили игру слов =)

По хорошему, вместо игры слов лучше использовать в сборке проекта continuous code quality подход с применением статического анализа кода и проверок следования стилю кодирования. Тогда упрощается и процедура ревью кода — код который нарушает правила просто не собирается в бранче на CI сервере. Есть ещё фреймворк, которые проверяют базовые архитектурные соглашения и точно так же "рушат" сборку при нарушениях.

Я согласен с тем, что continuous code quality классная вещь, вот только мне как разработчику для ускорения своей работы полезно знать правила по которым проходит оценка качества кода.
Безусловно, понимать нужно и, например, Sonar показывает что нарушено, почему это плохо и очень часто как нужно было делать, чтобы реализация была правильной с точки зрения этого правила. А плагин к вашей IDE позволит получать предупреждения в процессе написания кода.
При том, что SOLID сам по себе является аббревиатурой, состоящей из аббревиатур и включающий в себя Demetra's law — все это выглядит странно


Разве SOLID включает в себя Demetra's law?
UFO just landed and posted this here
То есть архетиктура акронима не удовлетворяет требованиям, которые он содержит :)

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


Фабрика — это абстрагированный конструктор. А значит, объект который фабрика вернула, должен считаться созданным в вызвавшем фабрику методе.


Более того, слепое следование рассуждениям из статьи может привести к очень кривому коду. Допустим, объект который вернула фабрика нужно закрывать. Как это будет выглядеть в простом случае?


foo = factory.create();
foo.bar();
foo.close();

Но если бездумно следовать закону Деметры — то вызовы bar и close придется вынести в отдельный метод… который в итоге будет закрывать объект не им открытый.

Вариантов море
1. RAII, в этом случае конструктор объекта создаст внутренние и close в этом случае адекватен, в рамках управляемого кода, в рамках же того же C++ вместо close будет деструктор.
2. DI, здесь close не будет сам закрывать, а просто дёрнет сервис для закрытия внутреннего объекта.
3. Я частенько в своём коде ипользую некие IGuard объекты, следящие за ресурсами, которые сами знают что и как делать.

Если же вы говорите вообще как close в противопоставление factory.create(), то здесь вы не ресурс уничтожаете, вы говорите, что надо бы вычистить внутренее состояние объекта, а не сам объект и это нормально.

В итоге в C# используем IDisposable для этих целей, по нему же и описание ловим и работаем так
using (var foo = factory.Creat())
{
  foo.bar();
}

В C++ используем std::unique_ptr как результат фабрики, само удалиться, тут по хорошему работать только с RAII.

И ничего для закрытия вручную не дёргаем.
Ну, под капотом using — обычный вызов Dispose, а под капотом RAII — деструктора. Не вижу причин почему генерируемый компилятором код имеет право нарушать правила.

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

Потому что этот код никто не читает.
Сгенерированный инкапсулированный код может нарушать все что угодно. Его не нужно поддерживать.

Не поймите неправильно, я ни в коем случае никого не призываю бездумно следовать тому, что описано в статье. Стоит помнить, что Закон Деметры, как и любой другой принцип стоит применять в меру и не вдаваться в крайности. Я описал следствия, которые действительно имеют место быть. Если в каком-то случае пренебрежение его использованием позволит улучить кодовое решение, то разумнее это сделать.
Критика подхода ограничения размера метода.
Просто ужасные советы если отнести к производительности. Напишите в таком виде алгоритм который осуществляет свёртку изображения по ядру произвольного размера. У вас там будет 4 вложенных фора в любом случае. А если несколько свёрток за раз по одному исходнику так 5.
Да и вообще есть миллион ситуаций где нельзя размазывать код. Сложные вещи останутся сложными. Вы можете спрятать эту сложность в тонне классов и кода, но лучше не станет. Станет ещё сложнее новичку увидеть и так сложный алгоритм сквозь размазню методов. Бить нужно логически так как бъётся алгоритм, так чтобы было читаемо и логически понятно что делается и как. А это приходит с опытом. А то бывает смотришь, всё вылизано по кодстайлам, куча маленьких методов, всё туда-сюда передаётся, идилия просто, каждый метод прозрачен как слеза младенца. Но хрен поймёшь что весь этот океан делает. Я не призываю всё сляпывать. Я призываю думать и обеспечивать прозрачность архитектуры и понятность алгоритмов, и это не правилом 15ти строк делается.
Чтобы правильно нарушать правила, нужно для начала научиться им следовать. Бить логически — сложно, это думать надо, и как вы правильно заметили — иметь опыт. С чего-то нужно начинать, чтобы этот опыт заполучить. Вероятно, новичку было бы полезно иметь набор простых советов из разряда «15 строк и не больше». Со временем придет понимание, что это далеко не всегда верно.
Касательно же производительности: в современном программировании производительность за пределами асимптотики нужна крайне редко. И даже в этом случае сначала было бы неплохо написать код понятным, а уже потом аккуратно профилировать и оптимизировать.
Не согласен. Применять практику не понимая зачем она странно. Имхо нужно упасть с велосипеда чтобы понять как ехать. В IT правда для этого надо поработать 2-3 года на одном проекте, чтобы самому оценить как работают твои же решения. Вот тогда появляется червячок мысли о том как читать книги по паттернам и на что на самом деле смотреть и как читать между строк.
Чтобы правильно нарушать правила, нужно для начала научиться им следовать. Бить логически — сложно, это думать надо, и как вы правильно заметили — иметь опыт.

Так чтобы научиться чему-то — надо это что-то делать. Если человек с самого начала будет бить бездумно методы, потому что "кодстайлы", то логическому разделению он никогда не научится.


Вероятно, новичку было бы полезно иметь набор простых советов из разряда «15 строк и не больше»

Совет должен выглядеть как: "если метод длиннее 15 строк, то стоит обратить на него внимание, и возможно вынести часть логики, если это будет разумно". И ни в коем случае не должен выглядеть как: "15 строк и не больше".

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

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

Проекты с огромным количеством лапши приносят деньги, в том числе программистам, проекты с красивым кодом, которые никогда не выходят в продакшн — закрываются.


Код должен сначала работать и зарабатывать, а потом быть "красивым". Если он плохо работает — надо улучшать, если появляются большие затраты на поддержку — надо улучшать.
Улучшать код до того, как он заработал — непозволительная трата денег заказчика.


Никто не говорит, что улучшать код не надо. Работаешь на поддержке — улучшай код, а не создавай еще больше лапши, кто тебе мешает?


Я, вот, работал на проектах поддержки, который писали более 5 лет до этого, и я улучшал код.

image
Но git помнит ваши имена ))
Если вы бы работали на поддержке, то бы знали, что любой рефакторинг запрещен в принципе, даже обычное переименование. И говно-код занимает больше времени, он даже больше по объему. И разве не проще сразу писать нормальный рабочий код?

Я работал на поддержке если что.
Даже больше скажу, сейчас у меня хобби-проект — моды к компьютерной игре. В модах есть унаследованный код, которому больше 5 лет, код других моддеров, который надо иногда править и обновлять (обновления о наших правках не знают ничего). Из инструментов — только текстовый редактор с подсветкой синтаксиса. Тестирования как такового нет, каждый тестовый цикл это запуск игры и около 3 минут ожидания.


И ниче: правим, рефакторинг делаем, добавляем и (о ужас!) убираем код. Около 300к строк.

Эм… Ну хз, у нас вот в принципе по максимуму запрещено менять любой код с модификаторами public, только в совсем крайних случаях можно. И это продукт с исходным кодом который клиентам отдается. А запрещено поскольку клиент мог сделать себе доработки которые на этот код опираются. А уж на поддержке и вовсе жуть может быть.

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

Давно уже придумали интерфейсы и SOLID-принципы… Клиенты не должны делать доработки внутри общего кода. Если у вас происходит такое, то вам срочно нужно от этого отказываться, т.к. это постоянный источник плохого качества кода и трудноуловимых багов… В идеальном случае клиенту предоставляется интерфейс/API для взаимодействия… А всё что внутри модуля может меняться постоянно и это нормально…

А представляете, как было бы замечательно, если бы код был понятнее и его поддержка занимала в 2 раза меньше времени, например. На самом деле, очень важно найти золотую середину: не писать лютый говнокод, но и не стараться добиться идеальной структуры кода, который даже не работает ещё. Все упирается естественно в ресурсы, в том числе времени, но важно помнить, что сэкономив время сейчас и набросав говнокод, его поддержка потом может съесть больше времени, чем вы выиграли на данном этапе. Так что важен баланс

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


А теперь самая важные фишка о которой все забывают:
1) "Идеальная архитектура" и "говнокод" это эмоциональные оценки. К формальным характеристикам сложности, связности, читаемости, скорости работы и плотности багов отношения не имеют.
2) "Идеальная архитектура" и "говнокод" это не противоположные стороны спектра. Может быть "говнокод" при почти "идеальной архитектуре".

«Идеальная архитектура» и «говнокод» это не противоположные стороны спектра. Может быть «говнокод» при почти «идеальной архитектуре»

Может. Потому что «идеальная архитектура» это идеальное разбиение программы на модули. И внутри модуля может иметься говнокод, но, разница не в этом, а в том что
— «идеальная архитектура» — при чистке говнокода ничего не ломается;
— «дерьмовая архитектура» — при чистке говнокода ломается ВСЁ и приходится лепить множество заплат;
— бывают и промежуточные состояния архитектуры.

Вы невнимательно читали. Я не писал ничего про архитектуру), я также не писал, что идеальный нерабочий код лучше, чем рабочий говнокод. А писал я про баланс. Если ваш бизнес не выделяет достаточно времени, чтобы писать нормальный код, то это может быть отправдано в следующих случаях: 1. У руководства есть понимание, что мы сейчас пишем быстро говнокод и в дальнейшем, если мы планируем поддерживать этот код, мы его причешем, перепишем и так далее. 2. Вы не планируете поддерживать написанный код, например, пишете прототип. Во всех остальных случаях руководство скорее всего не понимает проблему наличия говнокода и руководствуется планами только на ближайшую перспективу. Это, кстати, довольно распространенный подход. Хотя конечно, еще возможно, что денег много, и ничего страшного, если поддержка и внедрение фичей будут занимать больше времени. Ну тут, как говорится, "кто платит, тот и музыку заказывает", you are the boss)

Давайте считать, что срок фиксирован и всегда недостаточный, чтобы сделать все хорошо. Делать сразу, чтобы код был SOLID или написать быстро, чтобы работало, а потом поправить там, где архитектура совсем неочень получилась?


Я замерял, второй вариант получается быстрее. Получившийся код недостаточно соответствует канонам "правильной архитектуры". Но пользователю пофиг.

а потом поправить...

Если архитектура дерьмовая, то поправить что-то не сломав другое — не получится, в результате придётся лепить много заплат из-за «одно — лечим, другое — калечим».
При правильной архитектуре — можно вносить изменения ничего не ломая.

Поправить всегда можно. Код это же не кирпичи в стене нижнего этажа.

Если всегда, то почему при починке говнокода лезут новые баги и глюки?

У кого лезут? У меня не лезут.

Если не лезут — это как раз значит что архитектура более менее. Возможно вы просто не видели настоящее отсутствие архитектуры. Ну либо вы тратите например 5X времени вместо X.

Я много чего видел, в том числе то, что вы можете назвать "настоящее отсутствие архитектуры".


Ну либо вы тратите например 5X времени вместо X.

Чтобы потратить Х вместо 5Х при поддержке в "будущем" надо потратить 20Х на сейчас. "будущее" в кавычках, потому что оно может и не наступить.

Я много чего видел, в том числе то, что вы можете назвать «настоящее отсутствие архитектуры».

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

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


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


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


Проблема только в том, что такие действия требуют усилий по чтению и пониманию кода, а иногда и политической воли, чтобы внести изменения. А усилия и ответственность — не о, чего хотят программисты.

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

Ну так это уже можно сказать приемлемая архитектура, пусть еще и не хорошая.

Правильно. Я бы назвал это приемлемой (good enough) архитектурой. Даже из лютого говнокода можно сделать приемлемую архитектуру с небольшими усилиями.

Вот и я о том. А вот лютый говнокод и хреновая архитектура при которых как раз все и ломается в 20-ти местах при правке одного — это как раз то против чего я протестую. И вот эту приемлимую архитектуру лучше заложить сразу, чем страдать потом.

Так зачем протестовать — правьте. Причем не важно когда — хоть через 100500 лет после выпуска в продакшн, хоть сразу после написания рабочего кода.


Самое главное не надо пытаться это делать заранее.

Видимо либо предстоит второй круг либо зашли в тупик. Потому что я как раз считаю что на этом уровне не то что можно, обязательно делать приемлимую архитектуру заранее. А уже потом, когда требования уточнятся, пользователи будут использовать, архитектуру можно улучшать а можно (если прям заказчик готов платить за долгое внедрение новых фич за счет того что на начальной стадии продукт дешевле будет) и не улучшать.
Нет, не из любого. Если там нет архитектуры как таковой — то любое изменение требует огромных усилий, и «сделать приемлемую архитектуру» — не исключение.
Неужели писать код по SOLID настолько дольше?!
Или вы как в Delphi — весь код в button1_click пишите? Конечно важно как можно быстрее выдать продукт рынку, но через 1-2 года поддержка этого кода и добавление новых фич будет стоить компании в разы дороже, чем если бы сразу написать хотя бы приблизительно соблюдая SOLID

Когда нужно быстро — я всё равно делю код на классы (хотя бы уровня структура данных — контроллер/accessor — UI), для контроллеров делаю интерфейсы и резолвлю их через ServiceLocator/DI — если этого не делать, то потом юнит тесты замучаешься писать… Да и код по слоям разделён хотя бы…
Неужели писать код по SOLID настолько дольше?!

Зависит от степени упоротости в применении принципов SRP и DIP. В частности у меня был код, который в 10 строках читал данные из двух источников и записывал один. Чтение из каждого источника было одной строкой.
Посоны, которые делали по SOLID на каждую строку фиганули по классу. Потом выделили интерфейс, обратили зависимости. Потом из класса сделали базовый и два наследника, один тестовый, а второй обращался к системе. В итоге две строки превратились в двадцать две. Это не считая тестов.


Естественно две строки были написаны за 5 минут (вместе с проверкой), а 4 класса и инверсия зависимостей потребовали почти полдня работы.


Или вы как в Delphi — весь код в button1_click пишите?

Сейчас везде по дефолту MVC и MVVM, батонкликов не найдешь.


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

Сейчас везде по дефолту MVC и MVVM, батонкликов не найдешь.

А что там сейчас?
(так вышло, что с Дельфи перешёл на Оракл, и про то как сейчас не в курсе)
Ну ваш пример с SOLID — жесть конечно. Нарушает мои любимые принципы KISS и YAGNI. Чем больше кода напишите, чем тяжелее поддерживать. В данном случае те товарищи слишком буквально поняли SRP и вместо разделения ответственности стали плодить сущности… А YAGNI принцип нарушается, т.к. скорее всего никто не будет добавлять новые источники данных, а значит все эти интерфейсы будут лежать мёртвым грузом в проекте. Я бы рассмотрел эту задачу как конвертер, а значит обойтись одним интерфейсом IDataConverter, который умеет считать (не важно из скольки источников) и записать… Код бы стал на 4 строчки длинее и появилась бы возможность легко написать юнит тесты…

Почему же не поняли? Очень даже поняли, потом долго рассказывали что теперь в конфиге можно источники данных подменить, не трогая остальную логику.


С источниками данных, кстати, не самая большая проблема была. Просто самая показательная.

теперь в конфиге можно источники данных подменить

А насколько часто это нужно будет делать? Предполагаю, что «никогда». А значит это явное наружение принципов YAGNI, а именно создание и поддержка ненужного избыточного кода.
Сейчас везде по дефолту MVC и MVVM, батонкликов не найдешь.

Хахаха, вы серьезно? Как раз куча примеров когда все как раз в события формы лепится.
А вы можете еще куда-то налепить, если надо на событие формы отреагировать? :)
Ну так одно дело отреагировать, и другое — всю бизнес логику работы с данными в событии формы выполнять.
В обработчик команды «налепить» можно.
Мне правда жаль, что у вас всегда недостаточно времени для качественного выполнения своей работы. Это в большинстве случаев проблема менеджмента. Поменяйте работу, поверьте, есть компании, в которых и планирование адекватное, и при этом даже ЗП хорошую платят, ну и проекты уж точно не скучные. А пользователю действительно пофиг, как вы правильно заметили. Ему ж не придётся поддерживать говнокод. А вам придётся, ну или вашим коллегам.
«Идеальная архитектура» — это конечно субъективное понятие, а вот «говнокод» — вполне измеримое. Мои любимые принципы разработки: DRY, KISS and YAGNI. Т.е. любое изменение в коде должно происходить в одном месте (т.е. без необходимости использовать текстовый поиск и замену кода во многих файлах), код должен легко читаться и позволять понять его назначение, весь неиспользуемый код должен быть удалён. Для меня эти принципы важнее SOLID и прочих — и если они нарушаются — это и есть говнокод…

Если бы было проще писать "нормальный рабочий код", то его писали все. Но раз "нормального рабочего кода" оказывается в итоге крайне мало, то видимо не проще.


Не проще, потому что люди сначала долго рисуют диаграммы классов (иногда в голове), разделяют интерфейсы, инвертируют зависимости, делают так, чтобы код соответствовал OCP, SRP и еще каким-то аббревиатурам, а потом сроки подходят и срочно лепится абы-какой код, чтобы все взлетело.


Достаточно сделать просто в обратном порядке — сначала чтобы работало, а потом чтобы было красиво. Но для этого требуется скилл и выдержка.

Тут еще проблема, что поди все это объясни людям. У меня аж в трех компаниях подобная практика была, что сначала я пишу по задаче PoC код, причем и на планированиях отмечаю, что это proof of conect и в комментариях все помечаю, и в связной информации в системах контроля версий, трекерах и прочее, и каждый раз на меня смотрят «оО у нас так плохо и джуниоры не пишут». Разумеется, к PoC требований ну почти никаких, запросто бывает и полотнище размером в 1 метод на 1000 строк, и копипаста из интернетов, заместо каких-то уже библиотечных функций, и все константы не вынесены, ну не знаю, чем еще напугать, регистр переменных и идентификаторов рандомный, но зато, в среднем, это раз в 10 быстрее, чем писать сразу нормально, и отрефакторить это нормально займет не более трети времени от «сразу писать нормально».

Но нет. «А как же мы будем юнит тесты писать?» «У нас к каждому спринту юнит тесты — это обязательная часть acceptance criteria.»

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

MVP? Такого слова большинство коллег и не слышало. А менеджеры работают так, что если большинству что-то не нравится, значит — это что-то плохое.
А теперь два вопроса. В вашем коде при необходимости что то поправить сколько людей с ходу разберутся? И приводите ли вы его в порядок? А то может и падает то из за такого кода.
Вот, и Вы не понимаете. Прототипы не обязательно с ходу править другим людям. Ну то есть, их, главное не накапливать, а так, пока я, допустим, числюсь в компании, в худшем случае, если я внезапно исчезну, у них останется максимум один последний прототип, в котором действительно будет сложно разобраться остальным, и всегда можно договориться, чтобы носитель знаний описал в виде подробной документации, что же он уже выяснил на уровне прототипа, а что нет, и с чем появились реальные проблемы. И да, это некоторая потеря, но вполне решается тем, что прототипировать надо не вещи на следующий спринт, а иметь запас в несколько итераций (ага! вот тут-то вся проблема и есть).

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

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

У вас фигово процессы настроены. То, что вы не укладываетесь в сроки, в большинстве — результат плохого планирования. Мудрый менеджер возьмёт оценку программиста, умножит ее на 2 и ещё добавит половину, и даже такую оценку стоит брать во внимание с учётом других факторов, которые могут не зависеть от сроков выполнения задачи. А юнит тесты хороши, как вспомогательный инструмент, как гарантия,, что реализованный код работает правильно на данном этапе, ну и для устранения регрессий, но уж точно не как гарантия того, что ваш код и все остальное будет работать в продакшене в связке. Для этого есть ревью кода, процедура приемки со стороны QA, стейджинг и много всего ещё по желанию. А если у вас все это есть, но проекты продолжают падать в проде, значит у вас где-то что-то не отрабатывает как надо, поскольку до прода непротестированный проект при хорошо поставленных процессах просто не доедет) Даже с лютым говнокодом и без тестов можно жить, если есть правильно поставленная проверка QA на стейджинге) Не так конечно хорошо, как с тестами и не так быстро, потому что количество возвратов и багфиксов возрастает, но в целом можно. А вот наоборот — нет.

Конечно же — это проблема процессов, сам код-то ну как может быть виноват? Боюсь, что в наших реалиях, такая менеджерская мудрость не прокатит, ибо программисты обычно делятся на тех, кто совсем плохо оценивает, и умножив плюс-минус-неизвестно на 2.5, получим такое же плюс-минус неизвестно, либо оценивают хорошо, но получив реальные сроки в 2.5 раза больше необходимого, 1.5 часть времени будут страдать фигней, и за необходимый интервал все равно не уложатся, ибо каждый раз все упрется в какую-то непредсказуемую мелочь.

В целом, я видел, где-то процессы и работают, но там, обычно, нереальный overkill в плане manpower, или очень низкие темпы разработки (что суть одно в данном ключе), и процессы в основном, лишь для того, чтоб были. Либо же никакие процессы и идеологии не помогают все равно, ибо сроки сжаты.

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

Ну я и не спрою, конечно. Сложно верить, что процессы настроены верно, если в идеологии даже нет такого понятия, как прототипирование.
С математической точки зрения, лапша-код куда более интереснее для меня, с недавних пор). Так что не утверждаю, что такой код объективно хуже.
Раскройте тайну, что интересного в лапше с математической точки зрения? Мне тоже интересно.
натуральный Deus ex machina. Нынче вновь становятся популярны теория Хаоса и синергетика.
Думаю у каждого были интересные и весьма неожиданные баги. И в теории, можно получить такую систему, которая будет производить только положительные баги)
Задача программиста — упрощать сложную(системная сложность) систему и внедрить метод управления.
Для проектирования архитектуры и вообще главный принцип — это KISS и декомпозиция модулей.
Упрощение — это минимизация количества связей и возможных состояний системы. Для этого применяют инкапсуляцию и шаблоны проектирования, SOLID.
Для минимизации состояний используют минимизацию ветвления кода(все if) и декомпозицию методов(маленькие простые вложенные методы)
Для управления систем, чаще всего, используют классическую пирамиду власти из кибернетики.
Для понимания как нужно строить архитектуру и откуда берутся баги, нужно прочитать системный анализ. Прочитать можно за пару дней, никаких доп. знаний не нужно.

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

Позволю себе не согласиться. Не вижу ничего плохого в том, что бы думать об архитектуре в момент написания кода. У кода, как и бизнеса, есть одно постоянное свойство — меняться. Да, разумеется, иногда нужно по-быстрому сделать MVP и выпустить его в продакт, но по моему опыту нет ничего более вечного, чем временное. Если конечно у вас не конвеерное производство MVP, то закладывание возможности изменений принесёт больше пользы, чем времени отнимет. К тому же, следование описанным принципам со временем уже просто откладывается на подкорке и каких-то особых усилий не требует — проверено на себе и коллегах.

Плохо то, что программисты не могут остановиться. Улучшать код можно почти до бесконечности. А если кода еще нет, то мыслительный процесс может вообще не остановиться.


Многократно себя ловил на мысли о том, что начинаю улучшать код до его написания, что приводит к тому, что я не могу начать писать.


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

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

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

Не видите некоторого противоречия? Не закладывания возможностей, а закладывания возможности изменения кода. Это все таки разные вещи.

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

Нет, это не добавление кода «который не нужен сейчас, но возможно понадобится в будущем», а создание возможности легко добавить такой код в случае необходимости не сломав ничего.
Что достигается продуманной архитектурой = без кривого прибивания гвоздями «тяп-ляп и в продакшн».

Каким образом достигается "создаются возможности"? Из воздуха? Или есть некоторый код, который за это "создание возможностей" отвечает?

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

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


Но в жизни не встречал ни разу такого. Скорее было обратное. Ради "дополнительных возможностей" люди тратили в 10 раз больше времени, писали в 4 раза больше кода (а в совокупности с тестами в 8 раз больше), делали "идеальную" SOLID архитектуру, но код был нерабочим. Тупо не делал то, что нужно.

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

Или есть некоторый код, который за это «создание возможностей» отвечает?

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

Доказательство по аналогии ущербно изначально. Потому что надо сначала доказать верность аналогии.

Вы gandjustas сначала приписали другим "добавление кода, который не нужен сейчас, но возможно понадобится в будущем".
А когда вам сказали о том, что вы не правы и это не так — начали придумывать отмазки!
Слив засчитываем?

Я чето даже хотел ответить, но понял что смысла в этом нет

Если у вас на машине колёса — жёстко приколочены к оси по принципу «тяп-ляп и в продакшн», то вам чтобы на машине повернуть придётся поверх уже имеющихся колёс лепить «заплату» из дополнительных колёс, которые могут поворачиваться.

Если у вас требования ВНЕЗАПНО меняются с "приколотить колеса намертво" к "обеспечить возможность руления", то какие-то архитектурные недостатки (возможные) — наименьшая из ваших проблем. И уж точно не надо пытаться эту проблему решать архитектурными костылями.

UFO just landed and posted this here

Бывает что и из воздуха. Например, при использовании сторонней библиотеки или фреймворка.

Конечно излишнаяя приверженность — это плохо, но забивать на нее тоже нельзя, вообще сильно зависит от размеров проекта, если проект очень большой, то тут сразу лучше начать адекватно разрабатывать архитектуру, если проект постоянно меняется, то правильная архитектура поможет быстро выпилвать устаревший функциаонал и внедрить новый, легко протестировать все это итд. Если же проект не большой, то сильно можно и не заморачиваться и возможно получится все удержать в голове. По поводу преждевременной оптимизации — лучше бы люди все же иногда задумывались бы об оптимизациях, а то последнее время все полюбили это правило и вообще не оптимизируют ничего, что относительно простые программы тормозят на весьма не плохом железе.
Тут главное не запутаться. Излишней приверженности правильной архитектуры не бывает, ибо архитектура практически всегда, для любого приложения будет морфироваться в худшую сторону со временем. Ну кроме приложений «написал и выбросил», но имхо к ним вообще не может быть никаких требований кроме надежности/безопасности.

Другое дело, что стилистика написания кода к архитектуре имеет лишь условное отношение. И излишняя декомпозиция на ранних этапах, да, зло, не меньшее, чем преждевременная оптимизация. Хотя бы потому, что декомпозировать всегда проще, чем объединить уже разбитые (и особенно сомнительно) сущности.

А об оптимизациях, ну вот в восновном все и тормозит, ибо из-за слепых требований к объему методов, простоты форматов, у нас в доброй части приложений, чтобы 2+2 сложить, в 15 LOC методе эти данные сначала из JSON парсятся, потом обратно запихиваются, и чтоб скучно не было, это все еще сидит под жирным мьютексом, ибо чего плодить объекты синхронизации. Зато SRP, OCP, LSP, ISP, DIP, и прочее, что нужно каждому сеньору потрогать в продакшн на уровне эксперимента, чтобы потом понять, что в принципе, программирование никогда об этом и не было.
UFO just landed and posted this here
Именно, вспоминаются исследования приводимые макконелом в «Совершенный код», которые показывали что стоимость ошибок на каждом этапе: выявление требований->проектирование->конструирование повышается в разы, а то и на порядок.
Ошибки лучше предотвращать на стадии проектирования, чем потом выяснить что архитектура настолько кривая, что лучше бы переписать весь проект с нуля. :(

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

А тут вступает в игру другой фактор. Часто после того как накидали что то для проверки концепта — это что то остается в таком виде поскольку «Ну переписать это же ресурсы нужны, а у вас вон уже продукт есть, давайте быстро еще фичи.» И похрен что все страдают как из-за непродуманной бизнес логики, так и из за говнокода в который изменения просто страшно вносить. Ибо «что за тесты такие? Это же ресурсы нужны».
А тут вступает в игру другой фактор. Часто после того как накидали что то для проверки концепта — это что то остается в таком виде поскольку «Ну переписать это же ресурсы нужны, а у вас вон уже продукт есть, давайте быстро еще фичи.»

Вот видите. Продукт оказался востребованным, есть бюджет, есть требование новых фич. А если бы планировали — то никто бы новых фич не требовал, потому что продукт оказался бы нафиг никому не нужен к тому моменту, как вы там чего-то допланировали :)


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


Ну и:


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

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

А вот тут есть еще пара но. Таким образом шла разработка второй версии продукта, когда первой уже было лет 7, и она нормально продавалась. И страдаю не я, страдают пользователи и заказчики из за того что фичи долго внедряются и новые ломают старые из за кучи костылей и отсутствия тестов. Собственно этой второй версии уже почти 4 года и почти ничего не меняется.
А вот тут есть еще пара но. Таким образом шла разработка второй версии продукта, когда первой уже было лет 7, и она нормально продавалась.

Но ведь вторая версия не просто так появилась? В ней, видимо, возникла какая-то необходимость. Решили реализовать какой-то функционал, который давал бы конкурентное преимущество, так? Вы можете гарантировать, что если бы вторая версия с соответствующим функционалом появилась с задержкой, то продукт был бы так же успешен?


В общем, суть в том, что часто бывают рассуждения вида: "вот сразу не делали все по науке, не учли роста, а теперь приходится жрать кактус", но они не учитывают, что очень часто именно тот факт, что делали вначале "херакс-херакс и в продакшн", и позволил "вырасти" и жрать теперь кактус.

Я не пойму, вы всерьез считаете что если все время разработчиков тратится на исправление ошибок и ком только растет — это нормально? Что когда из соседних отделов людей выдергивать приходится на латание ошибок? И иногда еще фичи добавляются (если не добавить — вообще вздернут клиенты).
Окей, выпустили MVP, оценили что нужно, самое критичное реализовали, может пора уже и что нибудь поправить? Хотя бы избавиться от методов на тысячи строк, конструкций if… else вложенных в одной процедуре по 7-10 уровней, от функций сигнатура которых больше чем наполовину состоит из флагов которые передаются через 5 уровней вызова чтобы где нибудь влепить очередной костыль в if… else, и подобного?
Я не пойму, вы всерьез считаете что если все время разработчиков тратится на исправление ошибок и ком только растет — это нормально?

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


Что толку от грамотного планирования и затрат на контроль за архитектурой, если в итоге ваш проект просто не доживет до того момента, когда плохая архитектура могла бы оказать какое-либо влияние?


Окей, выпустили MVP, оценили что нужно, самое критичное реализовали, может пора уже и что нибудь поправить?

Может, и пора. Но, очевидно, есть и еще какие-то задачи (они всегда есть, согласитесь). Есть какой-то актор, ответственный за определение приоритета задач. Он не выбрал рефакторинг и исправление архитектуры. Возможно ли такое, что он основывался на каких-то объективных факторах?


Я не говорю, что плохая аритектура, недостаток планирования и некачественный код лучше, чем хорошая архитектура, грамотное планирование и качественный код. Очевидно, не лучше. Я говорю о том, что все имеет свою цену и вполне возможно, что в данный момент — она, ну, слишком велика. Возможно же?

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

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

UFO just landed and posted this here
UFO just landed and posted this here
> Кисло с нуля переписывать запутанное легаси в десятки тысяч строк, где заплатка сидит на заплатке и заплаткой погоняет. :(

Если переписывать с нуля — запутанность и количество заплаток не играет ровным счетом никакой роли :)
UFO just landed and posted this here

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

Проблема в том, что процесс проектирования кода, для соответствия принципам SOLID и GRASP не уменьшает количество ошибок.

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

Тут интересно отметить следующую штуку — вот в стандартах нередко вводятся понятия вроде: "обязательно к исполнению", "очень рекомендовано к исполнению", "рекомендовано к исполнению" (с четким описанием этих понятий). И, с-но, те или иные вещи маркируются соответствующими понятиями.
По логике-то ко всем принципам надо относиться тем же самым образом — есть какие-то вещи которые необходимо исполнять, есть которые можно нарушать в определенных случаях, есть "просто рекомендации", но нет же! Все всё воспринимают как: "шаг вправо/влево — расстрел, подпрыгивание на месте — попытка улететь".

Меня все чаще смущает поголовный отказ от


public function processSomeDto(SomeDtoClass dto)
{
     if (predicat) {
          throw new Exception(‘predicat is failed’);
     } else {
          return this.dtoProcessor.process(dto);
     }
}

И если этот пример я еще могу понять, так как в реальности это скорее всего было бы так:


public function processSomeDto(SomeDtoClass dto)
{
     if (predicat) {
          throw new Exception(‘predicat is failed’);
     } 
     if (anotherPredicat) {
          throw new Exception(‘another predicat is failed’);
     } 
     return this.dtoProcessor.process(dto);
}

И это хорошо, это точки выхода, которые легко вынести в что-либо вроде


public function processSomeDto(SomeDtoClass dto)
{
     this.validateParams()
     return this.dtoProcessor.process(dto);
}

Но основной посыл всегда — отказаться от else, если работать будет так же без него. А это имхо, зло.


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


function abs(int num){
     if(num < 0){
          return -num
     }
     return num
}

function abs(int num){
     if(num < 0){
          return -num
     } else {
          return num
     }
}

Одно дело точки выхода, другое дело акцент на разных поведениях и при рефакторинге, когда нет этого самого else — намного проще допустить ошибку

Это две разные ситуации. В случае с Exception — это "отсекающие" условия. В вашем примере уместнее оставить else.

Максимум один уровень вложенности на метод.

То есть цикл с break/continue вы написать не можете?

Как вы верно отметили
if (predicat) {
     break;
}
внутри цикла будет нарушением одного из принципов объектной калистеники. Но когда такая необходимость действительно возникает, я предпочитаю не впадать в крайности и не бросаться исключениями, а сознательно нарушить это правило и оставить как есть. Цель данного правила в том, что бы не усложнить код, а избавиться от многоуровневых лесенок. Для себя вы можете взять не цифру 1, а более. Я в практике стараюсь использовать 1 уровень. И каждое его превышение обосновать.

Печалька с ограничением вложенности и длины методов — падает code locality в исходнике. Вместо обозримого метода, пусть на 25 вместо 15 строк получаем пяток функций, а это лишние прыганья по коду туда-сюда, мучительное выдумывание этим функциям названий. Опять же, при необходимости поменять входные аргументы на глубоком уровне приходится менять все сигнатуры по пути, просовывать новые аргументы через все уровни, в результате дифф разбухает. В общем я готов поспорить с тем что искусственные ограничения на вложенность или длину функций являются безусловным добром.

Кроме того, как с подобными ограничениями можно реализовать хоть какой либо алгоритм? Если не нарушать принцип локальности, то элементарно посчитав количество вложенных for можно сразу сделать предположения о сложности алгоритма, но если размазать их на кучу вложенных методов с одним циклом, то… да что тут говорить, на реальной практике команда программистов в компании где я работал не замечала 3 года, что у них один из алгоритмов работы с данными имеет кубическую сложность, и элементарно приводится к O(nlogn), но как догадаться, если все размазно на методы по 15 LOC?

Кроме того, неизбежно, если мы пишем многопоточный код (а мы ведь пишем?) будет уйма проблем с синхронизацией/блокировкой. Запихивать блокировку в каждый 15 LOC метод жирновато, не правда ли? И дело не в том, что рекурсивные мьютиксы как-то плохо с этим справляются, вовсе нет, но если слишком часто отпускать блокировки, то в большинстве нетривиальных ситуаций мы лишь потеряем огромное время, если у нас больше одного связанного логикой объекта блокировки.

Понятно, что панацеи тут нет, но стратегия использовать любое количество LOC (любое — значит действительно любое) для метода у которого ровно одна ответственность, и никакая его внутренняя часть не решает никакой понятной и самодостаточной задачи для меня намного более предпочтительна, чем шинковать методы по любым правилам длины. Сейчас сложно найти редактор, который не умеет по уровням вложенности сворачивать, кроме того.
Для решения этой проблемы можно выделить некую обёртку, единственным назначением которой будет создание объекта с помощью фабрики и передача его на обработку куда-либо дальше.

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

Максимум один уровень вложенности на метод.

Есть такое понятие "цикломатическая сложность". Упрощённо — количество ветвлений и циклов в методе + 1. Во времена изучения мною этого вашего ООП рекомендовалась цикломатическая сложность не более 4-5 на метод.
Позднее, я сталкивался с написанием алгоритмов с цикломатической сложностью порядка 100 (специфическая обработка полилиний в ГИС). Это было реализовано примерно тридцатью методами с цикломатической сложностью в основном 4-6, иногда до 10.


В статье же предлагается ограничить цикломатичекую сложность значением около двух ("около" — потому что вложенность и цикломатическая сложность не эквивалентны). Для достаточно сложных алгоритмов это предложение звучит бредово.


Более того, с ограничением "1 уровень вложенности" даже линейный поиск не написать.
for(...){if(...){return ...}} — уже два уровня вложенности.

Есть такое понятие "цикломатическая сложность".

Оно работает только для структурной парадигмы. В ООП (из-за виртуальных вызовов) или ФП (из-за лямбд) цикломатическая сложность не определена. Точнее, если попытаться определить ее хоть сколько-нибудь разумным образом, то она будет выходить бесконечной.

Цикломатическая сложность вычисляется для одной функции, а не для программы целиком (потому что для программы целиком она не определена при наличии простейшей рекурсии).

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

Это не важно. Если в ф-и есть виртуальный вызов или лямбда — цикломатическая сложность не определена.


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

Правда? Вот вам два куска кода:


function test(x: number, y: number) {
    if (x > y) {
        console.log('bigger');
    } else {
        console.log('lesser');
    }
}

и:


function yobaIf<T>(pred: boolean, then: () => T, els: () => T) {
    return pred ? then() : els();
}

function test(x: number, y: number) {
    yobaIf(x > y, () => console.log('bigger'), () => console.log('lesser'));
}

какова цикломатическая сложность ф-и test в первом случае и какова во втором? если она в обоих случаях одинаковая — то ответьте, считаете ли вы способ расчета цикломатической сложности, при котором она в указанных ф-ях одинакова, "сколько-нибудь разумным", учитывая что после простейшей механической замены ифов на лямбды (продемонстрированной выше) цикломатическая сложность любой ф-и становится равной единице?

А еще если всю функцию простейше-механически перенести в другую оставив один вызов — то цикломатическая сложность тоже станет равной единице, но это почему-то никого не удивляет…
А еще если всю функцию простейше-механически перенести в другую оставив один вызов — то цикломатическая сложность тоже станет равной единице

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

Что же до вашего примера — то у измененной функции test на языке javascript формальная цикломатическая сложность — 1, общая же 3 (поскольку функций-то три), что даже больше чем у исходной или даже 5 если считать yobaIf.

Если же рассматривать эту функцию как программу на «птичьем языке» построенном над javascript — то ее цикломатическая сложность остается 2, ведь в этом языке yobaIf — условный оператор.

К Хаскелю понятие «цикломатическая сложность» не применимо потому что там нет алгоритмов в традиционном понимании этого слова.
Что же до вашего примера — то у измененной функции test на языке javascript формальная цикломатическая сложность — 1

Почему 1? как вы посчитали?


общая же 3 (поскольку функций-то три), что даже больше чем у исходной или даже 5 если считать yobaIf

Почему три? Это вы как посчитали?


на самом деле, в обоих случаях — ровно 2, т.к. у вас в графе 5 узлов, 5 ребер и 1 компонента связности


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

В смысле, нет? Конечно же, есть, точно такие же алгоритмы, как и везде. Вы имеете ввиду, что там нету процедур и ф-и все чистые? Так это как раз вычислению цикломатичесой сложности никак не мешает. Если запретить в хаскеле лямбды и ввести if спецформой, то прекрасно там все будет считаться.
Неприменимо оно к хаселю именно потому, что лямбды там по факту есть. И точно так же оно неприменимо ко всем остальным языкам с лямбдами, потмоу что вы не можете по ф-и построить ее control flow. Именно по-этому при определении цикломатической сложности строго оговаривается, что исследуемая функция — структурная.

Я бы все-таки ослабил это требование до требования императивности.
Я бы все-таки ослабил это требование до требования императивности.

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


Ну вы попробуйте сами логически рассудить — вот вы согласны, что в чистом бестиповом лямбда-исчислении цикломатическая сложность "не считается", так? Но ведь любую ф-ю, которую вы можете написать на чистой бестиповой лямбде вы можете написать точно так же в js! получается каким-то магическим образом одна и та же ф-я, написания одним и тем же образом при помощи одних и тех же примитивов и, в принципе, даже, в принципе, в одном и том же синтаксисе (ну нам же никто не мешает в лямбда-исчислении сделать синтаксис со стрелками вместо lambda-x.t?), то есть просто один и тот же терм (!) с той же самой семантикой (!) не имеет цикломатической сложности в рамках чистой лямбды, но сразу эту сложность преобретает, когда мы говорим: "а, так это написнао на js!"!
Вам не кажется это странным?

Чистые безтиповые лямбды не являются императивными независимо от того на каком языке написаны.
Чистые безтиповые лямбды не являются императивными независимо от того на каком языке написаны

То есть цикломатическая сложность для чистых ф-й не считается, вы так предлагаете? Вы сформулируйте четко, чтобы не было непоняток.

В ООП (из-за виртуальных вызовов) или ФП (из-за лямбд) цикломатическая сложность не определена. Точнее, если попытаться определить ее хоть сколько-нибудь разумным образом, то она будет выходить бесконечной.

Если виртуальный метод делает что-то в соответствии с его спецификацией, то не важно, виртуальный он или нет. Виртуальность (или не виртуальность) вызываемого метода не меняет число путей, по которым может идти выполнение вызывающего кода.
Или у вас есть пруфы, что цикломатическая сложность по определению не может использоваться с виртуальными методами?


Для ФП, наверно, эта метрика действительно не определена из-за отсутствия графа потока управления.
Также не вполне ясно, как считать её для всяких там цепочек map — filter — reduce или list comprehensions, которые сегодня есть в ООП-языках.


Но речь шла о процедурном и ООП-коде, а не о хаскеле.

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

Вообще-то зависит. Если у вас вызывается какой-то метод (виртуальный) и есть десяток классов в которых он реализован, то у вас 10 путей исполнения. А если 5 реализаций — то всего 5 путей исполнения.


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

Конечно, она может использоваться, но она не будет показывать что-либо осмысленное. Вы можете все if-ы заменить виртуальными вызовами через диспатч по таблице виртуальных ф-й и станет у вас везде единичная сложность. Какой смысл в такой метрике?


Для ФП, наверно, эта метрика действительно не определена из-за отсутствия графа потока управления.

Дык и виртуальный вызов и лямбда — это одного характера вещи. Лямбда-исчисление эмулируется на ООП (ну, как и наоборот).
Так что у вас если нету лямбд но есть ооп, то вы все ранво можете писть "как на хаскеле" (ну как на лиспе скорее, что не существенно в рамках дискуссии :))

Если у вас вызывается какой-то метод (виртуальный) и есть десяток классов в которых он реализован, то у вас 10 путей исполнения...

… за пределами оцениваемого кода.

… за пределами оцениваемого кода.

Но без которых не получится достроить граф. Вы же понимаете, что какая-нибудь лямбда в шарпожавах — это семантически тот же анонимный класс, реализующий интерфейс типа IRunnable? То есть я пишу на ООП себе лямбды, потом на этих лямбдах — пишу себе yobaIf и потом ломаю этим yobaIf вам расчет сложности. Вопрос же здесь в том, до каких пор можно использовать лямбды/ооп, чтобы "все было хорошо"? Ну вот когда мы уже до yobaIf дошли, то так уже, очевидно, нельзя, весь расчет смысл теряет, а где граница?
И тут сразу чтобы определиться — на самом-то деле я могу и не реализовывать лямбды в явном виде, а воспроизвести логику yobaIf неявно.


По всей видимости, граница там, где вы передаете лямбду аргументом. А передача лямбды аргументом — это ведь то же самое, что передача объекта, реализующего некоторый интерфейс. То есть пролегает эта граница практически сразу.

Да без проблем достраивается граф! Виртуальный вызов — это один оператор. Что он делает внутри — уже не важно.
Да без проблем достраивается граф! Виртуальный вызов — это один оператор. Что он делает внутри — уже не важно.

Нет, не достраивается. Если у вас есть ф-я А, в которой вы передаете некоторый объект, реализующий интерфейс B, в ф-ю C, то все реализации интерфейса B — узлы графа для А. При этом вы не знаете связей между узлами, не заглядывая в С. Ну, то есть, да, что в самом виртуальном вызове — не важно, важно, когда вы передали объект с возможностью виртуального вызова куда-то там.

Если у вас вызывается какой-то метод (виртуальный) и есть десяток классов в которых он реализован, то у вас 10 путей исполнения. А если 5 реализаций — то всего 5 путей исполнения.

Метрика рассчитывается только для написанного метода. Иначе, при использовании вашего подхода, нужно учитывать все пути во вложенных вызовах. Боюсь тогда даже представить цикломатическую сложность функции
void pf(float n){printf("%.3f\n", n);}
или какой-нибудь функции, содержащей recv().


Лямбда-исчисление эмулируется на ООП (ну, как и наоборот).

Естественно. Но я не уверен насчёт применимости метрики цикломатической сложности к коду, содержащему несколько map (т.е. циклов) в строчке. С одной стороны, код простой, с другой — в нём куча условий и циклов.

Естественно. Но я не уверен насчёт применимости метрики цикломатической сложности к коду, содержащему несколько map (т.е. циклов) в строчке.

Ну то есть вы согласны, что если в коде используется ф-я, в которую вы передаете другую ф-ю, то сложность ломается, т.к. по факту эта ф-я может быть семантически эквивалентна ветвлению или циклу, или чему-то еще более сложному.
Но передача ф-и аргументом в принципе ничем не отличается от передачи объекта, реализующего IRunnable.
То есть — если у вас есть ф-я, которая принимает ф-ю или ф-я, которая принимает объект с виртуальными методами, то все ломается. Но если исключить такие передачи то у вас фактически и останется чистое структурное программирование :)

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

Вообще-то, нет. Я считаю сложность для вызывающей функции. Её код (call *%rax) никак не меняется от того, сколько имплементаций есть у переданной функции. Из вызывающей функции все вызываемые выглядят одинаковыми чёрными ящиками, независимо от их числа или реализаций. Поэтому вызов на сложность не влияет.


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

Upd.: switch(...){...} и (*f)(); могут иметь схожие графы потока управления, но вызов функции имеет одну и ту же семантику для всех её реализаций, все они выполняются одинаково для вызывающего кода. А ветки в switch — это разные варианты, их все нужно тестировать отдельно. Я вижу в этом разницу.

Вообще-то, нет. Я считаю сложность для вызывающей функции. Её код (call *%rax) никак не меняется от того, сколько имплементаций есть у переданной функции.

Дык а как вы считаете сложность вызывающей ф-и, если не можете построить ее граф без ф-и вызываемой?


function test(x: number, y: number) {
    yobaIf(x > y, f, g);
}

откуда вы знаете, какие дуги соединяют узлы yobaif, f и g, если не знаете реализации yobaIf?


Из вызывающей функции все вызываемые выглядят одинаковыми чёрными ящиками

Только в том случае, если вызываемая ф-я не принимает объектов и лямбд. Если она принимает — то она уже не черный ящик, т.к. она меняет (потенциально) граф потока управления той ф-и, из которой вызывается. Вот в чем проблема.


Upd.: switch(...){...} и (*f)(); могут иметь схожие графы потока управления, но вызов функции имеет одну и ту же семантику для всех её реализаций, все они выполняются одинаково для вызывающего кода.

ну можно написать так, что (*f)() будет выполняться для вызывающего кода точно так же как switch. В чем тогда разница?

откуда вы знаете, какие дуги соединяют узлы yobaif, f и g, если не знаете реализации yobaIf?

Внутренних дуг между ними — нет. Это известно.

> Внутренних дуг между ними — нет. Это известно.

Откуда? Если у меня yobaIf(pred, f, g) { f(); g(); }, то будет граф yobaIf -> f -> g, то есть как раз внутренняя дуга.
что значит «внешние»? не бывает никаких внешних дуг у графа. Есть узлы yobaIf, f, g. Все дуги, которые соединяют эти узлы, принадлежат графу, который содержит эти узлы. У вас после узла yobaIf выполнение переходит на узел f а потом на узел g. С-но дуги графу принадлежат, иначе как управление-то переходит?

Не понимаю, почему вы делаете именно такое различие между виртуальной функцией (учитываете граф вызываемой функции) и обычной (не учитываете графа вызываемой функции). Особенно когда речь идёт о метрике вообще другой функции, вызывающей. И если учитывать, что статическая функция из стандартной dll может иметь десяток реализаций.


ну можно написать так, что (*f)() будет выполняться для вызывающего кода точно так же как switch. В чем тогда разница?

Примеры:


1:
obj->f();

2:
switch(obj->type){
case 0: a(obj); break;
case 1: b(obj); break;
case 2: c(obj); break;
case 3: d(obj); break;
case 4: f(obj); break;
case 5: d(obj); break;
case 6: a(obj); break;
case 7: b(obj); break;
case 8: f(obj); break;
default: obj->p();
}

Не находите, что блок-схема во втором случае слегка отличается?

Не понимаю, почему вы делаете именно такое различие между виртуальной функцией (учитываете граф вызываемой функции) и обычной (не учитываете графа вызываемой функции).

Тут вышло недопонимание, речь не о вызове obj->f() речь о вызове p(obj), внутри которой уже, в свою очередь как-то вызывается (или не вызывается, мы не знаем) obj->f(). Чтобы построить граф ф-и, в которой совершен вызов p(obj) нам надо знать, как, с-но, внутри р происходят вызовы методов obj.

Мне кажется, вы рассматриваете граф путей выполнения с точки зрения процессора или анализа производительности (но я всё равно не понимаю, почему вы делаете исключение именно для вирт.функций).
Однако метрика "цикломатическая сложность" применяется для оценки сложности кода с точки зрения написания и понимания, а не выполнения процессором. Поэтому вызов функции не добавляет сложности вообще, независимо от его виртуальности.

Однако метрика "цикломатическая сложность" применяется для оценки сложности кода с точки зрения написания и понимания, а не выполнения процессором. Поэтому вызов функции не добавляет сложности вообще, независимо от его виртуальности.

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


Вот я взял ф-ю с большой сложностью, заменил там все ифы и циклы на виртуальные вызовы или лямбды. Код при этом изменился только синтаксически и совсем немного. Цикломатическая сложность стала 1. Что вам это число показывает в данном случае? Ф-я же проще не стала, она, может, сложнее стала, но точно не проще.


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


Если ф-и структурные, вы можете сказать: "ага, вот тут цм 5, а тут цм — 15. первая функция норм, а вторая — сложная, надо подумать, может ее переписать?". В случае анализа в ООП/ФП у вас вполне может оказаться, что сложность ф-и с цм 5 выше чем сложность ф-и с цм 15, а когда вы перепишите цм 15 в цм1 то в итоге может оказаться что вы сложность не уменьшили а просто замели под ковер, спрятав за лямбдами/виртуальными вызовами, то есть сделали еще хуже чем было. Я вот об этом говорю.
Оценка по цм работает в структурном программировании, потому что там граф потока управления нагляден. В ООП/ФП вы можете куски графа управления тасовать и перекидывать туда-сюда.
То есть когда кто-то в ФП/ООП считает цикломатическую сложность и что-то рефакторит, чтобы ее уменьшить — это просто карго-культ, человек не понимает, что он делает.

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


Уточните, пожалуйста, про невозможность подсчёта цикломатической сложности при наличии виртуальных вызовов — это вы сами придумали или это где-то описано в литературе? Было бы интересно почитать. Ваша аргументация ясна, но не могу с ней полностью согласиться.

И считается, что это уменьшает сложность кода (особенно когда таких switch было много по всему коду).

Но на самом-то деле не уменьшает, вот в чем штука :)


Уточните, пожалуйста, про невозможность подсчёта цикломатической сложности при наличии виртуальных вызовов — это вы сами придумали или это где-то описано в литературе?

Это прямое следствие из определения цикломатической сложности. Явно оно, конечно, нигде не написано, в силу своей очевидности, но известно любому знакомому с основами cs человеку, который когда-то цикломатическую сложность пытался смчитать :)
Можете сами взять определение, попробовать посчитать и наглядно убедиться, что ничего не получается :)

Но на самом-то деле не уменьшает, вот в чем штука :)

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

Так ведь нет.


В текущем методе больше не нужно отслеживать разные ветки switch, мы их в другой метод скрываем.

С чего бы? У вас остается тот же самый свитч, просто вы ветки исполнения разбрасываете по всему коду вместо того, чтобы держать их в одном месте.
Это упрощает понимание в том случае, если ветки одинаковы по семантике, то есть все ветки свитча делают одно и то же действие, параметризованное параметром свитча, но без возможности записать это в виде ф-и от параметра в силу ограниченной выразительности конкретного ЯП.
Если же у вас какой-то алгоритм, который в разных ветках свитча предпринимает семантически разные действия, то вы только все усложните, т.к. вам чтобы понять алгоритм придется по куску все ветки свитча собрать.

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

Эм… Почему? Из текущей процедуры он исчезает. И в текущей процедуре, не включая вызываемые процедуры, количество путей выполнения снижается.
Из текущей процедуры он исчезает.

Куда исчезнет?

В ту процедуру в которую мы switch вынесли.

А мы вынесли? Вроде, в описанном вами способе нету никакой такой процедуры, куда оказался вынесен свитч, разве не так? :)

Да, тут косячнул, не обратил внимания на «виртуального», совсем другой рефакторинг.
Можете сами взять определение, попробовать посчитать и наглядно убедиться, что ничего не получается :)

Много раз считал.
На английской вики есть ссылки на исследования, в которых метрика цикломатической сложности успешно применяется в ООП-проектах.


Не вижу больше смысла спорить с тем, что вам "очевидно".

Много раз считал.

Значит, неправильно считали. Вот вам ф-я:


function someFunction(arg: ISomeInterface) {
    justFunctiion();
    arg.someMerhod();
}

Если вы считали цикломатическую сложность, то должны знать, что дуга в flow графе обозначает переход на инструкцию. Начинаете вы с начала ф-и, start, в случае justFunction все понятно — вы переходите на start JustFunction, а на какую инструкцию вы переходите при вызове someMethod()?


в которых метрика цикломатической сложности успешно применяется в ООП-проектах.

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

А какая разница? Для понимания текущей функции нам это неважно. Мы знаем эта функция делает justFunctiion, а также делегирует someMerhod на arg, все.
А какая разница? Для понимания текущей функции нам это неважно. Мы знаем эта функция делает justFunctiion, а также делегирует someMerhod на arg, все.

Мы сейчас не про понимание ф-и, а про вычисление цикломатической сложности. Если нет конца дуги — нет флоу графа, нет флоу графа — цикломатическая сложность не рассчитывается. Ф-я, конечно, может при этом быть простой, кто спорит-то.

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

Для ее расчета нам важно, сколько путей в соответствующем cfg. А cfg у этой ф-и не существует. Нельзя посчитать пути в графе, которого не существует.

Нарисуйте, пожалуйста, блок-схему этой функции.

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

Мне кажется, все проще.


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

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

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

Ну прекрасно, я объявил ф-и где-то в другом месте и только их использую, то есть:
yobaIf(pred, f, g), где f и g — определеные где-то там ф-и. Или даже: yobaIf(pred, f(x), g(x)), где х — обычная переменная (не ф-я), а f и g возвращают ф-и. Какая в данном случае сложность?

В обоих случаях либо 1 либо 3. С одной стороны, путь выполнения один — вызов yobaIf с аргументами. С другой, аргументы сами являются ссылками на выполняемый код, то есть являются возможными путями выполнения. Думаю, логично будет считать, что сложность 3, так как пути выполнения обозначаются здесь.
Думаю, логично будет считать, что сложность 3, так как пути выполнения обозначаются здесь.

А почему именно 3? откуда вы знаете что там столько путей выполнения? Может, их 1? или 23? Откуда вообще взялось число три, как вы его получили?

Один тело самой функции, один первый callback, один второй callback. Число путей выполнения это те, которые задаются здесь. То, что задается снаружи функции, при оценке сложности того что внутри нас не интересует.
Один тело самой функции, один первый callback, один второй callback.

Так какая разница сколько колбеков? Это же не пути выполнения, это узлы в графе, и внутри вашей ф-и никаких путей между этими узлами не задано. 2 в данном случае — это просто случайное число. Можно так же было бы сказать, что их 100 или 42, ничем не хуже числа. А чтобы узнать, сколько между этими узлами путей — вам уже нужно заглянуть в реализацию вызываемой ф-и.

Это возможные пути выполнения. Как и ветки в if. Вы же не знаете в момент анализа, какая из веток выполнится.
Внутри нашей функции они связываются оператором вызова другой функции. 2 это число кусков кода, которые могут выполняться. Очевидно, 100 или 42 числа хуже, так как они не показывают число кусков кода.
Внутри анализируемой функции у них один путь — в вызываемую функцию.

Это возможные пути выполнения.

Еще раз, это не пути выполнения — а узлы. Точки, между которыми можно пути проложить. Или не проложить. Вы понимаете разницу между путем в графе и узлом в графе?


Вы же не знаете в момент анализа, какая из веток выполнится.

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


Очевидно, 100 или 42 числа хуже, так как они не показывают число кусков кода.

А какое отношение число кусков кода имеет к количеству путей через эти куски кода? То есть, отношение определенно есть, но какое? Вы сможете сформулировать?


Внутри анализируемой функции у них один путь — в вызываемую функцию.

Если путь один, то почему вы считаете 2? Что-то я утратил нить повествования. Вы можете четко и ясно определить ваш способ расчета? Вы пути считаете или не пути? Если пути, то какие и в чем?

Вы понимаете разницу между путем в графе и узлом в графе?

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


"Цикломатическая сложность части программного кода — количество линейно независимых маршрутов через программный код."
Функция это линейно независимый маршрут? Да. Ссылка на нее находится в программном коде функции? Да. Значит каждую можно считать за 1.
И самое главное что проблема с вынесением if в функцию исчезает. Раньше были куски кода в фигурных скобках, которые считались за 1, и теперь они есть.


Может, никакая вообще не выполнится. Может, выполнятся обе.

if (inputA == inputB)
if (inputC == inputD)


Ровно то же самое. Не будем их считать?


А какое отношение число кусков кода имеет к количеству путей через эти куски кода?

Они сами по себе линейно независимые куски, которые присутствуют в коде функции. Их я и считаю.

> Я понимаю разницу между понятным кодом и не очень. Нужно брать метрику, которая это отражает, а не гнаться за формальными определениями.

Верно! Я ведь с этого и начал. Цикломатическая сложность отражает сложность кода в случае структурных программ. В случае использования ООП и ФП ее нельзя ввести так, чтобы она отображала сложность кода и сохранить при этом локальность.
Конкретно по вашей методе нам придется считать все возможные перестановки по всем возможным узлам, это n! путей, где n — количество используемых лямбд. Если у вас 4 лямбды? То сложность уже сразу станет 24. А если это, например, всего-навсего 4 селектора?

Нет. 4 лямбды — 4 участка кода. Сложность 4. Такая же как и у switch с 4 блоками.

> Нет. 4 лямбды — 4 участка кода. Сложность 4. Такая же как и у switch с 4 блоками.

Так почему вы считаете участки кода, если надо считать пути между этими участками? По-вашему свитч на 4 кейза эквивалентен свитчу на 24? Ну то есть если у вас, например, 4 свитча, внутри каждого из которых еще по 6, то это по-вашему будет так же 4, так?

Свитч на 4 кейса — это switch на 4 куска кода (линейно независимых маршрута). 4 свитча, внутри каждого из которых еще по 6 — это 24 куска кода. Лямбда это один кусок кода. Если тело находится внутри нашей функции, можно дополнительно посчитать внутреннюю сложность лямбды, и соответственно рассматривать полную и собственную сложность функции.


Я же говорю, надо отталкиваться от целей, а не от формул. Был switch на 4 кейса — была сложность 4. Заменили switch на вызов функции с 4 лямбдами — должна остаться сложность 4. Если ваша формула здесь не работает, значит надо менять формулу.

Я же говорю, надо отталкиваться от целей, а не от формул.

Так я вроде тоже об этом.


Давайте по порядку — вот была у нас структурная программа с if, мы заменили if на ф-ю, и вычисляемая "стандартным способом" цикломатическая сложность снизилась, хотя сложность программы (воспринимаемая интуитивно) — очевидно, нет, ведь код по факту остался тем же самым.


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


Пересчитываем — все хорошо, теперь замена коснтуркции на ф-ю ничего не меняет.


Хорошо, говорю я, теперь давайте рассмотрим ф-ю, заменяющую конструкцию со сложностью 24. Была программа со сложностью 24, поменяли на ф-ю — бах, опять там 1! Непорядок. Значит, думаю я, надо считать сложность этой ф-и 24, по аналогии с if, и все становится хорошо.
Нет, говорите вы, надо считать 4, а не 24 (????)
и тут я логику рассуждения теряю. Почему 4? Должно же быть 24? Если мы выбираем 4 — то не нарушаем ли мы тем самым изначально выбранный принцип о том, что при замене конструкции со сложностью n ф-ей, сложность этой ф-и должна считаться n, чтобы осмысленно отображать сложность программы? Или у нас его и не было? А какой был и откуда он взялся?

мы заменили if на ф-ю, и вычисляемая "стандартным способом" цикломатическая сложность снизилась

В определении ясно сказано "количество линейно независимых маршрутов через программный код". Лямбда это линейно независимый маршрут. С моей точки зрения, "стандартным способом" всё нормально вычисляется.


Была программа со сложностью 24, поменяли на ф-ю — бах, опять там 1.

Нет. Мы рассуждали о 2 вложенных свитчах на 4 и на 6. Выносим первый свитч в функцию, заменяем ветки первого на лямбды — получаем 4, у каждой лямбды внутренняя сложность 6. Выносим оба свитча в функцию, заменяем все ветки на лямбды — получаем вызов функции с 24 лямбдами, сложность 24.
Если мы просто убираем оба свитча в лямбду вместе с кодом веток, не передавая его снаружи, то это просто вынесение в отдельную функцию, очевидно, что в изначальной функции останется сложность 1. Ну и плюс собственная единица оцениваемой функции во всех случаях.


Пример для первого случая:


function f()
{
    switch_func($condition,
        [$this, 'callbackA'],
        [$this, 'callbackB'],
        [$this, 'callbackC'],
        [$this, 'callbackD']
    );
}

Сложно считать, что у нее сложность 24.

> Лямбда это линейно независимый маршрут.

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

> Нет. Мы рассуждали о 2 вложенных свитчах на 4 и на 6. Выносим первый свитч в функцию, заменяем ветки первого на лямбды — получаем 4, у каждой лямбды внутренняя сложность 6.

Нет, не так. Выносим и получаем 4 лямбды, потому что в ветках свитча — не отдельные лямбды, а _их перестановки_ из всех 4. 24 перестановки. функция switch24 которая принимает 4 лямбды и заменяет конструкцию из свитча на 24 кейза с 24 перестановками.
Если бы у вас был свитч — вы бы посчитали сложность 24. Но вот вы заменяете этот кейз эквивалентной фвп — и становится сложность 4?

(подключусь к срачу дискуссии повторно)


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

На практике, это "сколько угодно" обычно ограничено весьма небольшим числом (аналогично, не бывает у класса бесконечного числа потомков с переопределением вирт.метода, обычно потомков "несколько").


Ещё раз выражу мнение, что метрика вводится для оценки куска кода — насколько он мудрёный для понимания. Уменьшить сложность кода для понимания можно путём ведения абстракций. При вводе абстракций сложность отдельных кусков кода снижается, общая сложность программы возрастает. Это свойство соответствует "моему" (и я, кажется, не одинок) определению цикломатической сложности (число ветвлений + число циклов + 1), так что на практике с "моим" толкованием всё в порядке: когда мы выделяем достаточно сложный метод B() из длинного метода A(), сложность метода A() уменьшается на сложность метода B().


В вашей же трактовке, если мы считаем сложность программы как число путей в общем графе выполнения, при выделении сложного метода B() из не менее сложного метода A() сложность A() не снижается. так как в вызове прячется многое. То есть, выделение абстракций не уменьшает сложность (по вашей метрике) метода A(). Это свойство вашей трактовки не совпадает с (общепринятым) мнением, что выделение метода B() из A() уменьшит сложность (да даже укоротит) метода A().


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


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

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

Нет, в моей трактовке сложность снижается, при условии, что метод B() не принимает в качестве аргумента объектов с виртуальными методами или лямбд (что одно и то же).
Если же метод B() принимает объекты или лямбды — то мы просто не можем для ф-и А() посчитать цикломатическую сложность так, чтобы она давала какую-то полезную информацию о сложности метода.

сложность снижается, при условии, ...

Это как-то нетривиально. Откуда вы это берёте?


Если же метод B() принимает объекты или лямбды — то мы просто не можем для ф-и А() посчитать цикломатическую сложность так, чтобы она давала какую-то полезную информацию о сложности метода.

Откуда возьмутся различные лямбды в методе A() для передачи в метод B()? Наверно, конкретная лямбда определяется всё-таки ветвлением? И тогда в коде


def B(f):
  print(f(3))

def A():
  if something:
    f = lambda x: x*2
  elif something_else:
    f = lambda x: x+2
  else:
    f = lambda x: x**2
  B(f)

метод A() будет иметь цикломатическую сложность 3, B() — сложность 1. (т.к. конструкции if, def, while, for, операторы ||, && добавляют по 1)


Или вы считаете, что сложность будет больше?

Это как-то нетривиально. Откуда вы это берёте?

Беру что именно?


метод A() будет иметь цикломатическую сложность 3, B() — сложность 1.

А если у вас B(pred, f) = when pred f()?
Тогда использование b это то же самое, что и использование конструкции when и должно повышать сложность А. Но мы же не можем заглянуть в В, сохраняя локальность цикломатической сложности? Значит, и не можем узнать, какую сложность В должно добавлять. Значит просто говорим: "хз".
Вот я про что.

Беру что именно?

Вот это:


Нет, в моей трактовке сложность снижается, при условии, что метод B() не принимает в качестве аргумента объектов с виртуальными методами или лямбд (что одно и то же).

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


А если у вас B(pred, f) = when pred f()?

Не встречал таких конструкций, извините. Это какой язык?

То, что в зависимости от реализации B() сложность метода A() у вас снижается или не снижается.

Нет, она снижается или не снижается в зависимости от вызова метода B. Эта строчка метода А, не В. Что там в реализации В мы не смотрим, там может быть что угодно. может, он вообще все аргументы игнорирует? Мы не знаем.


Не встречал таких конструкций, извините.

Ну это не важно встречали или нет. Что будет, если таковая возникнет?


Это какой язык?

Никакой, просто псевдокод.

Нет, она снижается или не снижается в зависимости от вызова метода B. Эта строчка метода А, не В.

Кажется, у вас виртуальность метода B() относится к реализации метода A(), а не B(). Но на уровне исходного кода, к которому обычно применяются метрики, виртуальный и не виртуальный метод вызываются идентично. У потока выполнения в методе A() нет другого выбора (пути), кроме как зайти в метод B(), независимо от того, зашит ли адрес метода B() в код (статический метод) или вычисляется (виртуальный метод). Вы же (насколько я понимаю), напротив, утверждаете, что тут принимается решение, по какому пути идти. Но оно уже принято ранее, вычисление только загружает адрес в регистр IP.


Ну это не важно встречали или нет. Что будет, если таковая возникнет?

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

Лямбда — это не маршрут.

Неправда. Она такой же маршрут как и ветка if или switch.


Это узел, через который может проходить сколько угодно маршрутов

Ну тогда и через if может проходить сколько угодно маршрутов. А уж сколько их проходит через while(true) + switch…
Меня не интересует, сколько раз вызывается лямбда. Для ее понимания мне надо просмотреть ее один раз.


Выносим и получаем 4 лямбды, потому что в ветках свитча — не отдельные лямбды, а их перестановки из всех 4

Я не очень понял эту часть. Можете пример привести до и после? Ну и для предыдущего пункта тоже.

Неправда. Она такой же маршрут как и ветка if или switch.

Маршрут откуда и куда, извините? Вы граф в данном случае вообще как представляете?


Ну тогда и через if может проходить сколько угодно маршрутов.

Через if может проходить только два линейно независимых маршрута. Точно так же через цикл. Через ф-ю, которая принимает n лямбд может проходит (потенциально) n! линейно независимых маршрутов.


Я не очень понял эту часть. Можете пример привести до и после? Ну и для предыдущего пункта тоже.

был свитч:


switch n {
    case 1: { f1(); f2(); f3(); f4(); break; }
    case 2: { f2(); f1(); f3(); f4(); break; }
    case 3: { f2(); f1(); f4(); f3(); break; }
    ...
    case 24: { f4(); f3(); f2(); f1(); break; }
}

мы поменяли на case24(n, f1, f2, f3, f4);


Ну и для предыдущего пункта тоже.

Для предидущего это для какого?

Вы граф в данном случае вообще как представляете?

Она сама по себе отдельный граф. Компонента связности кажется это называется.


Через if может проходить только два линейно независимых маршрута

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


мы поменяли на case24(n, f1, f2, f3, f4);

А, ок, я представлял лямбды с содержимым между фигурными скобками в case. Вы как-то незаметно ушли от примеров, аналогичных yobaif.


Для этого случая сложность 4. Сложность 24 целиком ушла в case24. Общая сложность увеличилась и это логично, так как case24 более абстрактная, чем исходный кусок кода.


То что они вызываются несколько раз в новой функции, ни на что не влияет. Так же как и вызов несколько раз в цикле.


Для предидущего это для какого?

Для "сколько угодно маршрутов". Но ок, теперь понятно.

В таком случае через лямбду проходит только один независимый маршрут — из начала в конец.

Но по факту же не один. В примере выше через каждую лямбду проходит много разных маршрутов.


Вы как-то незаметно ушли от примеров, аналогичных yobaif.

Да вроде такой же пример. Не вижу качественной разницы.


Для этого случая сложность 4.

Но почему 4-то, если для аналогичной языковой конструкции, которая выглядит и записывается точно так же, будет 24? Тогда, получается, посчитанная вами сложность не показывает "интуитивно воспринимаемую" сложность программы (что мы и хотим измерить). Она ведь будет здесь значительно больше 4. Я же из ваших предыдущих слов верно понял, что вы согласны с тем, что наша метрика должна отражать интуитивно воспринимаемую сложность?

В примере выше через каждую лямбду проходит много разных маршрутов.

if ($cond) {
    f1();
    f2();
} else {
    f2();
    f1();
}

Функции f1() и f2() это куски кода. Через них проходит несколько маршрутов. От этого их сложность как-то увеличивается? Нет.


Да вроде такой же пример. Не вижу качественной разницы.

Такой же был бы с ветками в виде лямбд.


cond24(n,
    function(){ f1(); f2(); f3(); f4(); },
    function(){ f2(); f1(); f3(); f4(); },
    function(){ f2(); f1(); f4(); f3(); },
    ...
    function(){ f4(); f3(); f2(); f1(); },
);

24 лямбды, сложность 24. И в cond24() тоже сложность 24.


Но почему 4-то, если для аналогичной языковой конструкции, которая выглядит и записывается точно так же, будет 24?

Она не записывается точно так же, как вызов case24(). Языковая конструкция записана в case24(), и сложность 24 ушла вместе с ней. То есть, если считать суммарный код, сложность 24 превратилась в 28. А также появилась возможность подменить функции fN() на другие.

Функции f1() и f2() это куски кода.

ф-и f1 и f2 это куски кода ф-й f1 и f2. А в рассматриваемой ф-и куски кода — это вызовы ф-й. И этих вызовов 4 штуки (два для f1 и два для f2). Через каждый вызов проходит только один путь.


Она не записывается точно так же, как вызов case24(). Языковая конструкция записана в case24(), и сложность 24 ушла вместе с ней.

Тогда почему сложность конструкции if не уходит туда, где она записана (в yobaIf)?

Через каждый вызов проходит только один путь.

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


Тогда почему сложность конструкции if не уходит туда, где она записана (в yobaIf)?

Потому что это разные примеры. У вас case24 не может принимать произвольные ветки, в отличие от yobaif.
В моем варианте ветки передаются 24 лямбдами, и их сложность тоже 24. То есть тоже не уходит.

Это какой язык? Код похож на с++, но это не c++, поскольку в с++ нет никаких «методов» (В ISO/IEC 14882 нет такого термина).
Это какая-то смесь Java и PHP.
нет, в ActionScript как минимум типизация не так указывается
Я когда эту литературу читаю, то все время вхожу в когнитивный диссонанс изза неточных переводов. Например Inversion — в английском имеет больше смыслов, чем в русском.
ссылка на словарь. Больше по смыслу подходят «перестановка; изменение порядка;»
Это вы про Dependency inversion principle?

Почему вы считаете что «перестановка зависимостей» является более точным переводом чем «инверсия зависимостей»?
Инверсия в русском — это что-то делать наоборот. Например, крутил вправо, потом стал крутить влево. А inversion of conrol больше смысл «передача управления».
в русском инверсия используется, когда есть 2 варианта — например, логическая инверсия бита 0 <--> 1. Инверсия управления — в русском как-то не звучит, есть более подходящие фразы. Больше ассоциируется толи с обратным исполнением кода от последней строки к первой, толи с переворачиванием чего-то.

Так ведь там именно переворачивание и происходит!
а вызов процедуры — это тоже инверсия контроля?
> Так ведь там именно переворачивание и происходит!

Нет, там как раз «переворачивания» (в смысле из A -> B стало B -> A) никакого нет. Так что замечание вполне корректное.
В отчете ревьюера будет написано: как Боженька написал.
class DIP
{
     private $service;

     public function DIP(SomeServiceInterface $someService)
     {
          $this->someService = $someService;
     }

     public function goodMethod()
     {
          // operations with someService
     }
}

Ну это спорная история. Вы только что stateless сущности на ровном месте сделали состояние. И тут надо понимать, а стоило ли прямо на уровне объекта делать эту зависимость, или можно было в метод её передать, и в нём же сразу использовать.


С самим принципом я согласен, но пример выбран не очень удачно, я считаю.

Вы только что stateless сущности на ровном месте сделали состояние. И тут надо понимать

А где тут состояние?

Sign up to leave a comment.