И в этом less/sass/scss/stylus просто вся верстка всего сайта зашита. Сколько раз я приходил в проекты и выпиливал там все подобное к чертовой матери, потому что ну разумеется там любое изменение в дизайне проходило через стадию «это невозможно сделать в нашем подходе» к стадии "!important", «style=''''» и наконец «style=''!important''».
Как правило, «желтый» — это несколько оттенков для фона, текста и рамок. Выполнить полную замену по папке src с $text-red -> $text-blue не очень сложно. Я сам, конечно, полностью поддерживаю наименование переменных исходя из контекста и семантики, а не значения, а то так недолго докатиться до for (var zero = 0; zero < n; ++zero)
Парсер не нагружает, парсер работает один раз и все подобные вещи индексирует. А вот увлекательные сайдэффекты связанные с неожиданным весом селекторов, жесткой привязкой порядка вложения тегов друг в друга и невалидной версткой (в li нельзя блочные элементы) — наличествуют.
Очень не однозначная фича, не находите? Вы можете придумать бизнес кейс для этого
1. Backend-case: есть микросервис, который общается с SMTP-сервером. Есть другой микросервис, который должен зарегистрировать пользователя, в числе прочего отправив письмо ему на мыло. Если в настоящий момент микросервис отправки писем не работает, то мы либо не сможем создать пользователя совсем, либо создадим пользователя и не отправим ему письмо. Первый вариант не совсем fault-tolerant, второй вариант вообще неприемлим. Если пользоваться MQ, то можно отправить письмо в момент подъема сервиса/SMTP.
2. Frontend-case: допустим, у меня есть сайт с TODOList-ом, который я написал, чтобы продемонстрировать работу вашего подхода. В нем два микросервиса — непосредственно список и счетчик завершенных задач, первый написан на реакте, а второй на vuejs. Никакого сервера вообще не имеется, и я публикую события в духе «создано новое туду» или «туду сделано». Но в какой-то момент микросервис счетчика упал окончательно и бесповоротно. К счастью, он сохранил свое состояние в локалстораже и при подъеме сможет себя из него восстановить. Но вот дела, пока он лежал без чувств, другой микросервис отправил вникуда пяток сообщений «новое туду», и юзер рискует увидеть несоответствие списка счетчику.
Нет, это не сумбурно и понятно — проблему версионности уместнее всего решать, собственно, версионностью.
Я думал о ситуации, когда новая версия категорически отметает обратную совместимость. Такое ведь тоже бывает? Никому не нравится видеть у себя в коде кучу устаревших контрактов, даже если эти контракты в отдельной сборке (субъективненько).
Позвольте сразу продолжить другую ветку здесь же, дабы не распыляться.
Ну я пытался перечислить две вещи, да, собственно их и перечислил. А независимость я не упомянул, поскольку эту проблему вы успешно решили, судя по содержанию статьи.
Насчет fault-tolerance. Допустим, один микросервис упал, а другой в это время к нему обращается с целью например что-то обновить в себе. Единая шина умеет в MQ? Если нет, мне кажется, это было бы очень круто, научить ее отправлять в «свежеподнятый» компонент все неполученные им в момент простоя сообщения.
А вы рассматриваете вариант, когда бизнес сам по себе сложный? Ну например какая-нибудь универсальная ERP или биллинг? Задача UI — быть простым, а для бизнеса (который представляет backend) таких требований обычно не ставится.
Внутри что-то шевелится, пытаясь как-то намекнуть, что в микросервисах одна из самых главных вещей это все-таки масштабируемость и fault-tolerance, а в однопоточном фронте это несуществующая проблема, но это холивор и к делу не относится.
А вопрос вот такой — в микросервисах в мире backend есть достаточно обычная ситуация:
1. Есть три микросервиса: А, В и С
2. Микросервис А меняет контракт одной своей точки входа окончательно и бесповоротно.
3. Сервис В сидит в соседней комнате и поддерживают новый контракт.
4. Сервис С сидит в другой стране и не успел поддержать новый контракт.
5. При деплое разворачивается две версии микросервиса А: новая и легаси, а с помощью service discovery сервисы В и С обращаются к разным инстансам этого микросервиса.
6. Утром просыпается команда С и обновляет версию, после деплоя которой легаси-версия сервиса А прибивается.
У вас не возникает такой проблемы, особенно с единой шиной взаимодействия всех микросервисов? Как вы решаете проблему версионности?
Если тест работает медленно, потребуется больше времени на проверку функционала. Это плохо.
Но если тест работает медленно потому что так устроен бизнес-процесс, что же в этом плохого? Если бизнес состоит из «нажать кнопку в выпадающем меню, дождаться, когда оно впадет обратно, нажать другую кнопку», то какие же проблемы подождать? Тест занимает не больше времени, чем SLA, так или иначе.
Такой себе выбор. :)
Вы так сформулировали, будто я говорил, что разработчики продукта должны тюнить его под тесты. Но это просто диаметрально противоположная моей точка зрения — я же всю дорогу распинаюсь, что тесты должны быть стабильными, а не продукт (ну типа тесты для того и нужны, чтобы находить нестабильности продукта).
Я ничего не сказал про идеальный код, обратите внимание, есть ощущение, что у вас маякнуло сначала «паттерны», а потом «SOLID», но это просто примеры. Идеальный код нужен не для стабилизации тестов, а для их поддержки, но бизнесу как правило наплевать на условия содержания автотестов, так что я даже и не собираюсь спорить с финальным тезисом.
Про первый пример — ну если пользователь не умеет так быстро работать, то зачем тест так работает? Можно например написать приложение, в котором после ввода пароля есть только 50мс на клик «отправить», тест справится, а юзер нет. Вы же должны тестировать не интерфейс, а взаимодействие предполагаемого юзера с ним, нет?
И про веб та же история — если сайт начинает работу только после прогрузки всех скриптов, то это должно быть учтено в тестах и явно видно всем.
Про страшные слова (солид я просто приплел в качестве примера) — это мастхэв для любых тестов. Типа такие скрижали: тесты должны быть зелеными, они должны проверять только что-то одно, тестовые данные должны быть неизменными для разных прогонов одного теста, результат прогона одного теста один раз должен полностью совпадать с результатом прогона этого теста более одного раза.
Подмена целей — ну окей. По моему опыту поиск багов нестабильными тестами отнимает времени у более дорогих автотестеров и разрабов больше чем, ручное протыкивание всего регресса. Баги они находят — но, как вы сами сказали, причиной нестабильности нередко является несоответствие тестов SLA (слишком быстро кликнули, не дождались чего-то итд). Но я вас понял.
1. Отказоустойчивость должна быть не к вашему коду, а к внешним зависимостям, которыми вы пользуетесь. Если webElement.click() не срабатывает в одном из пяти случаев, и причиной тому какая-то ошибка вебдрайвера, то лучше завернуть его в политику, чем оставлять все тесты, в которых кликается кнопка, с вероятностью 20% ложного отказа.
2. UI-тесты для веб-приложений являются по сути e2e-тестами, а значит характеризуют собой SLA данного приложения. Если в SLA прописано «вот эта кнопка может не сработать с вероятностью 10%», то тест должен именно это и проверять — запускаться условные 100 раз и проверять, что падений не больше 10. Но я честно еще не видел рандомизированного бизнеса.
3. Если тест падает из-за недоступности/ошибок внешней системы — это совершенно нормально. Если тест падает из-за ее нестабильности — это уже повод а) пересмотреть SLA, б) отказаться от внешней системы. Если мои e2e-тесты не имеют доступа к стабильному тестовому окружению, то зачем вообще они нужны?
По пункту 2. Я имел в виду не design patterns, к которым относятся упомянутые вами Singleton и Factory, я говорил о паттернах написания кода (например, SOLID, соглашений по именованиям или code review), а также тестирования (атомарность, идемпотентность, детерминированность) и дебага (логи), о которых вы здесь и писали. Напомню, что в контексте своего комментария я скорее говорил об опыте взаимодействия с автотестерами, а не о вашей статье. Просто меня триггернуло «любой автоматизатор подтвердит, что UI-тесты по своей природе и так не отличаются стабильностью. И это нормально». Вот я могу подтвердить, что многие автоматизаторы так считают, но это ненормально.
Вы подменяете конечную цель абстрактным «все тесты зеленые». Никому не нужны зеленые UI-тесты, всем нужно соблюдение SLA. Если у пользователя есть сценарий «пытаться нажать на кнопку, пока она не заработает» — чудесно, тогда ваш подход работает. Как правило, в UI-тестах еще и отваливается какой-то определенный шаг, и полный перезапуск сценария не будет соответствовать попыткам юзера совладать с плавающей ошибкой очереди или внешнего сервиса.
TLDR: Если какая-то часть вашей системы нестабильна by design, то UI-тесты должны повторять ожидаемое поведение пользователя при встрече с нестабильностью. И это должно быть частью теста, а не частью подхода к тестам.
Часто встречаю у автотестеров тему про «тесты нестабильны по своей природе, поэтому что они иногда падают — это нормально». Сам в свое время писал UI тесты на Selenium под дотнет и у меня появилось впечатление, что:
1. Критическая нестабильность Selenium или условного chromedriver это какой-то миф. Они иногда ведут себя странно, но самая банальная WaitAndRetry политика устойчивости к ошибкам решает порядка 90% проблем, когда остальные проблемы — это либо некорретное использование Selenium (например клик по существующей в DOM, но закрытой чем-то при отрисовке кнопке), либо уже баги системы, которые можно и нужно репортить авторам фреймворка/драйвера.
2. Как правило, разработчики не идут в автотестеры, а скорее наоборот, так что у автотестеров просто повальный дефицит знаний паттернов как написания кода, так и тестирования и дебага (подтверждением тому наличие пунктов про вывод человекопонятных ошибок, атомарность тестов и детерминированность тестовых данных, хотя они и абсолютно корректные, но очень уж «нубские», в духе «не называйте ваш тест Test1»).
Если один и тот же набор тестов на соседних прогонах имеет шанс выдать разные результаты, то это значит, что весь набор тестов бесполезен. Это не только «ой, этот тест Х постоянно падает, не парься», это еще и кейс «тест был зеленым по той же причине, по которой тест Х постоянно падал».
Ох, не понимаю я этого стремления ввернуть ToList. Скажем, в древние времена, когда ToArray пользовался древним ArrayBuffer'ом и вызывал аллокации чуть ли не на каждый элемент, в отличие от List<>, который это делал логарифм раз, я еще понимаю. Но в нашем современном мире ToArray делает то же самое, что ToList, только действительно тратит меньше памяти и вдобавок ограничивает в действиях с коллекцией — я бы тоже на ревью спросил «а почему тут используется лист? ты собираешься добавлять туда еще что-то или убирать?» Потому что как правило эти ToList'ы просто нужны для неотложенного итерирования.
Я согласен, что эта разница — попытка сэкономить на спичках и в формулировке «меньше памяти» может вызвать только улыбку, но с точки зрения чистоты кода и ограничения на изменение коллекции — я бы спросил с автора.
Опять же, зависит от обстоятельств: может быть это метод типа
public IReadonlyList<MyClass> Filter(IQueryable<MyClass> someData)
{
return someData.Where(x => x.IsValid).ToList();
}
тогда по сути все равно — что ToList, что ToArray.
Вопрос скорее в том, что возможно вы делаете что-то не так, если используете для генерации хтмл в мвц приложении не razor. Я понимаю, что бывают ситуации, когда дешевле просто нарисовать тегбилдером что-то, но чтобы это были такие большие блоки кода, как в вашем случае…
Вы же в курсе, что HtmlHelper умеет рендерить partial view? И что razor templates компилируются, если вас беспокоит производительность.
Отрицательный вес не нужен, это просто вопрос начала отсчёта, и в статье есть упоминание нормализации и сравнение с 0.5.
Если вес связи n, то начало отсчёта считается за n/2. Тогда присутствие фичи увеличивает "шанс" на n/2, а отсутствие — уменьшает на ту же величину. При идеальных условиях матожидание нормализованной сети составит 0.5. Вуху.
Если позволите, я от себя добавлю ещё одну причину, зачем нужен код ревью: сокращение цикла обратной связи. При этом прошу заметить что это не единственный способ, и он не даёт существенных преимуществ, если используется в одиночку.
Код ревью позволяет находить уязвимости и недостатки кода до того как с кодом начинает работать другой разработчик (а именно это приводит к упомянутым вами незапланированным активности). Есть и другие практики, сокращающие цикл обратной связи — например парное программирование.
На мой взгляд, используя кодревью как инструмент валидации стилистики кода, команда рискует утонуть в болоте "у нас так принято".
Про обучение новичков я вообще молчу.
Ну а новички, которые на ревью песочат "местных" это какие-то мифические звери, имхо.
А вынесение подобных результатов в отдельные абстракции (напомню, что я говорил именно про это, а не про вред промежуточных переменных) позволит вам также покрыть это тестами, что значительно упростит и рефакторинг, и дебаг, и поддержку кода.
Все бывает ненужным, даже весь код целиком.
Декомпозиция сама по себе не подразумевает вынесение каждой строчки в отдельную функцию, вы просто доводите мой пример до абсурда и он, разумеется, теряет свой смысл.
Смотрите, допустим у нас есть некоторых размеров функция, в которой присутствует несколько локальных переменных. Насколько я понял, автор оригинальной статьи называет «лишними» те переменные, от объявления которых можно избавиться, никоим образом не повлияв на рантайм приложения.
Например:
var buzz = function () {
var data = getSomeData();
var result = doSomething(data);
return result;
}
Некоторые линтеры сами предложат заменить это на
var buzz = function () {
return doSomething(getSomeData());
}
А в каком-нибудь абстрактном функциональном языке это будет выглядеть и вовсе так:
buzz = getSomeData * doSomething
Но за этими getSomeData и doSomething вовсе не обязана стоять одна строчка кода (в таком случае ее и так можно заинлайнить, наверное).
Опять же, насколько я понял, автор любит функциональное программирование, а во многих функциональных языках переменные отстутсвуют в принципе. Если себя «программировать» под их регулярное использование, то изучение этих языков может стать большой проблемой.
Когда видишь только агрегирование данных, без промежуточных переменных, куда сложнее держать в голове всю цепочку действий, тогда как с промежуточными переменными легче понимать что творится в коде.
Не холивора ради, но таки подобные задачи еще лучше решаются через декомпозицию. Если ваша функция состоит из нескольких шагов, которые совершенно очевидно делятся на части пресловутыми промежуточными переменными, то вы очевидно сможете разбить ее на несколько функций, каждая из которых будет удовлетворять условию.
И в этом less/sass/scss/stylus просто вся верстка всего сайта зашита. Сколько раз я приходил в проекты и выпиливал там все подобное к чертовой матери, потому что ну разумеется там любое изменение в дизайне проходило через стадию «это невозможно сделать в нашем подходе» к стадии "!important", «style=''''» и наконец «style=''!important''».
for (var zero = 0; zero < n; ++zero)1. Backend-case: есть микросервис, который общается с SMTP-сервером. Есть другой микросервис, который должен зарегистрировать пользователя, в числе прочего отправив письмо ему на мыло. Если в настоящий момент микросервис отправки писем не работает, то мы либо не сможем создать пользователя совсем, либо создадим пользователя и не отправим ему письмо. Первый вариант не совсем fault-tolerant, второй вариант вообще неприемлим. Если пользоваться MQ, то можно отправить письмо в момент подъема сервиса/SMTP.
2. Frontend-case: допустим, у меня есть сайт с TODOList-ом, который я написал, чтобы продемонстрировать работу вашего подхода. В нем два микросервиса — непосредственно список и счетчик завершенных задач, первый написан на реакте, а второй на vuejs. Никакого сервера вообще не имеется, и я публикую события в духе «создано новое туду» или «туду сделано». Но в какой-то момент микросервис счетчика упал окончательно и бесповоротно. К счастью, он сохранил свое состояние в локалстораже и при подъеме сможет себя из него восстановить. Но вот дела, пока он лежал без чувств, другой микросервис отправил вникуда пяток сообщений «новое туду», и юзер рискует увидеть несоответствие списка счетчику.
Я думал о ситуации, когда новая версия категорически отметает обратную совместимость. Такое ведь тоже бывает? Никому не нравится видеть у себя в коде кучу устаревших контрактов, даже если эти контракты в отдельной сборке (субъективненько).
Позвольте сразу продолжить другую ветку здесь же, дабы не распыляться.
Ну я пытался перечислить две вещи, да, собственно их и перечислил. А независимость я не упомянул, поскольку эту проблему вы успешно решили, судя по содержанию статьи.
Насчет fault-tolerance. Допустим, один микросервис упал, а другой в это время к нему обращается с целью например что-то обновить в себе. Единая шина умеет в MQ? Если нет, мне кажется, это было бы очень круто, научить ее отправлять в «свежеподнятый» компонент все неполученные им в момент простоя сообщения.
А вопрос вот такой — в микросервисах в мире backend есть достаточно обычная ситуация:
1. Есть три микросервиса: А, В и С
2. Микросервис А меняет контракт одной своей точки входа окончательно и бесповоротно.
3. Сервис В сидит в соседней комнате и поддерживают новый контракт.
4. Сервис С сидит в другой стране и не успел поддержать новый контракт.
5. При деплое разворачивается две версии микросервиса А: новая и легаси, а с помощью service discovery сервисы В и С обращаются к разным инстансам этого микросервиса.
6. Утром просыпается команда С и обновляет версию, после деплоя которой легаси-версия сервиса А прибивается.
У вас не возникает такой проблемы, особенно с единой шиной взаимодействия всех микросервисов? Как вы решаете проблему версионности?
Но если тест работает медленно потому что так устроен бизнес-процесс, что же в этом плохого? Если бизнес состоит из «нажать кнопку в выпадающем меню, дождаться, когда оно впадет обратно, нажать другую кнопку», то какие же проблемы подождать? Тест занимает не больше времени, чем SLA, так или иначе.
Вы так сформулировали, будто я говорил, что разработчики продукта должны тюнить его под тесты. Но это просто диаметрально противоположная моей точка зрения — я же всю дорогу распинаюсь, что тесты должны быть стабильными, а не продукт (ну типа тесты для того и нужны, чтобы находить нестабильности продукта).
Я ничего не сказал про идеальный код, обратите внимание, есть ощущение, что у вас маякнуло сначала «паттерны», а потом «SOLID», но это просто примеры. Идеальный код нужен не для стабилизации тестов, а для их поддержки, но бизнесу как правило наплевать на условия содержания автотестов, так что я даже и не собираюсь спорить с финальным тезисом.
Вот это уже интересно, подписываюсь =)
И про веб та же история — если сайт начинает работу только после прогрузки всех скриптов, то это должно быть учтено в тестах и явно видно всем.
Про страшные слова (солид я просто приплел в качестве примера) — это мастхэв для любых тестов. Типа такие скрижали: тесты должны быть зелеными, они должны проверять только что-то одно, тестовые данные должны быть неизменными для разных прогонов одного теста, результат прогона одного теста один раз должен полностью совпадать с результатом прогона этого теста более одного раза.
Подмена целей — ну окей. По моему опыту поиск багов нестабильными тестами отнимает времени у более дорогих автотестеров и разрабов больше чем, ручное протыкивание всего регресса. Баги они находят — но, как вы сами сказали, причиной нестабильности нередко является несоответствие тестов SLA (слишком быстро кликнули, не дождались чего-то итд). Но я вас понял.
2. UI-тесты для веб-приложений являются по сути e2e-тестами, а значит характеризуют собой SLA данного приложения. Если в SLA прописано «вот эта кнопка может не сработать с вероятностью 10%», то тест должен именно это и проверять — запускаться условные 100 раз и проверять, что падений не больше 10. Но я честно еще не видел рандомизированного бизнеса.
3. Если тест падает из-за недоступности/ошибок внешней системы — это совершенно нормально. Если тест падает из-за ее нестабильности — это уже повод а) пересмотреть SLA, б) отказаться от внешней системы. Если мои e2e-тесты не имеют доступа к стабильному тестовому окружению, то зачем вообще они нужны?
По пункту 2. Я имел в виду не design patterns, к которым относятся упомянутые вами Singleton и Factory, я говорил о паттернах написания кода (например, SOLID, соглашений по именованиям или code review), а также тестирования (атомарность, идемпотентность, детерминированность) и дебага (логи), о которых вы здесь и писали. Напомню, что в контексте своего комментария я скорее говорил об опыте взаимодействия с автотестерами, а не о вашей статье. Просто меня триггернуло «любой автоматизатор подтвердит, что UI-тесты по своей природе и так не отличаются стабильностью. И это нормально». Вот я могу подтвердить, что многие автоматизаторы так считают, но это ненормально.
Вы подменяете конечную цель абстрактным «все тесты зеленые». Никому не нужны зеленые UI-тесты, всем нужно соблюдение SLA. Если у пользователя есть сценарий «пытаться нажать на кнопку, пока она не заработает» — чудесно, тогда ваш подход работает. Как правило, в UI-тестах еще и отваливается какой-то определенный шаг, и полный перезапуск сценария не будет соответствовать попыткам юзера совладать с плавающей ошибкой очереди или внешнего сервиса.
TLDR: Если какая-то часть вашей системы нестабильна by design, то UI-тесты должны повторять ожидаемое поведение пользователя при встрече с нестабильностью. И это должно быть частью теста, а не частью подхода к тестам.
1. Критическая нестабильность Selenium или условного chromedriver это какой-то миф. Они иногда ведут себя странно, но самая банальная WaitAndRetry политика устойчивости к ошибкам решает порядка 90% проблем, когда остальные проблемы — это либо некорретное использование Selenium (например клик по существующей в DOM, но закрытой чем-то при отрисовке кнопке), либо уже баги системы, которые можно и нужно репортить авторам фреймворка/драйвера.
2. Как правило, разработчики не идут в автотестеры, а скорее наоборот, так что у автотестеров просто повальный дефицит знаний паттернов как написания кода, так и тестирования и дебага (подтверждением тому наличие пунктов про вывод человекопонятных ошибок, атомарность тестов и детерминированность тестовых данных, хотя они и абсолютно корректные, но очень уж «нубские», в духе «не называйте ваш тест Test1»).
Если один и тот же набор тестов на соседних прогонах имеет шанс выдать разные результаты, то это значит, что весь набор тестов бесполезен. Это не только «ой, этот тест Х постоянно падает, не парься», это еще и кейс «тест был зеленым по той же причине, по которой тест Х постоянно падал».
Я согласен, что эта разница — попытка сэкономить на спичках и в формулировке «меньше памяти» может вызвать только улыбку, но с точки зрения чистоты кода и ограничения на изменение коллекции — я бы спросил с автора.
Опять же, зависит от обстоятельств: может быть это метод типа
тогда по сути все равно — что ToList, что ToArray.
Вы же в курсе, что HtmlHelper умеет рендерить partial view? И что razor templates компилируются, если вас беспокоит производительность.
Отрицательный вес не нужен, это просто вопрос начала отсчёта, и в статье есть упоминание нормализации и сравнение с 0.5.
Если вес связи n, то начало отсчёта считается за n/2. Тогда присутствие фичи увеличивает "шанс" на n/2, а отсутствие — уменьшает на ту же величину. При идеальных условиях матожидание нормализованной сети составит 0.5. Вуху.
Если позволите, я от себя добавлю ещё одну причину, зачем нужен код ревью: сокращение цикла обратной связи. При этом прошу заметить что это не единственный способ, и он не даёт существенных преимуществ, если используется в одиночку.
Код ревью позволяет находить уязвимости и недостатки кода до того как с кодом начинает работать другой разработчик (а именно это приводит к упомянутым вами незапланированным активности). Есть и другие практики, сокращающие цикл обратной связи — например парное программирование.
На мой взгляд, используя кодревью как инструмент валидации стилистики кода, команда рискует утонуть в болоте "у нас так принято".
Про обучение новичков я вообще молчу.
Ну а новички, которые на ревью песочат "местных" это какие-то мифические звери, имхо.
Декомпозиция сама по себе не подразумевает вынесение каждой строчки в отдельную функцию, вы просто доводите мой пример до абсурда и он, разумеется, теряет свой смысл.
Смотрите, допустим у нас есть некоторых размеров функция, в которой присутствует несколько локальных переменных. Насколько я понял, автор оригинальной статьи называет «лишними» те переменные, от объявления которых можно избавиться, никоим образом не повлияв на рантайм приложения.
Например:
Некоторые линтеры сами предложат заменить это на
А в каком-нибудь абстрактном функциональном языке это будет выглядеть и вовсе так:
Но за этими getSomeData и doSomething вовсе не обязана стоять одна строчка кода (в таком случае ее и так можно заинлайнить, наверное).
Опять же, насколько я понял, автор любит функциональное программирование, а во многих функциональных языках переменные отстутсвуют в принципе. Если себя «программировать» под их регулярное использование, то изучение этих языков может стать большой проблемой.
Не холивора ради, но таки подобные задачи еще лучше решаются через декомпозицию. Если ваша функция состоит из нескольких шагов, которые совершенно очевидно делятся на части пресловутыми промежуточными переменными, то вы очевидно сможете разбить ее на несколько функций, каждая из которых будет удовлетворять условию.