Чистые функции

Чистый код @ Borda: Том II

Эта статья является второй из серии статей о чистом коде. В этой статье мы узнаем, как кодировать чистые функции.

Отдел компьютерных наук Университета Юты определяет функции как автономные модули кода, выполняющие определенную задачу. Ключевое слово здесь — задача. Не задачи, а единичная задача. Это соответствует основному принципу Чистого кода:

Функции должны делать одну вещь. Они должны делать это хорошо. Это должны делать только они.

- Роберт С. Мартин

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

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

  • Маленький
  • Не повторяйтесь
  • Используйте описательные имена
  • Аргументы
  • Логические флаги
  • Организация
  • Структура функции
  • Блоки и отступы
  • Не иметь побочных эффектов
  • Разделение запросов команд
  • Обработка ошибок
  • Мертвый код

В Borda мы стараемся максимально следовать учению дяди Боба. Однако, основываясь на решениях, которые мы принимаем как команда, мы не боимся идти разными путями; пока они не нарушают основы Чистого Кода. Ниже я подробно рассмотрю каждое правило и объясню, как мы применяем его в Borda, если применимо.

Сохраняйте функции небольшими

Дядя Боб повторяет:

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

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

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

Не повторяйтесь

Дублирование может быть корнем всех зол в программном обеспечении.

Эта цитата дяди Боба является свидетельством эффекта дублирования кода.

Код считается дубликатом, если:

  • Это выглядит так же, как некоторые другие функции
  • Он делает то же самое, что и некоторые другие функции

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

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

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

Используйте описательные имена

В Borda наша философия такая же, как у дяди Боба: объясните себя в коде.

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

«В компьютерных науках есть только две сложные вещи: аннулирование кеша и присвоение имен вещам».

— Фил Карлтон

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

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

«Код подобен юмору. Когда приходится объяснять, это плохо».

— Кори Хаус

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

Аргументы

Сократите количество аргументов

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

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

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

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

Избегайте использования флаговых аргументов

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

В Borda мы следим за тем, чтобы в производственном коде не было аргументов-флагов. Тем не менее, мы немного снисходительнее относимся к нашему тестовому коду, особенно к фиктивным данным для нашей среды модульного тестирования. Обоснование состоит в том, чтобы генерировать фиктивные данные с небольшими вариациями, не требуя отдельных методов, которые включали бы почти дублирующую логику. До сих пор это работало для нас, но мы открыты для изменения нашей стратегии, как только это начинает мешать нам.

Держите их организованными: правило шага вниз

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

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

Структурируйте свою функцию

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

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

Блоки и отступы

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

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

По словам дяди Боба, блоки кода внутри операторов if, else и while должны быть минимальными, желательно до одной строки, и эта строка должна предпочтительно быть вызовом функции. Хотя мы в Borda последовательно следуем этому правилу, мы немного гибки в отношении того, сколько строк может содержать if. Пока выполняемые действия тривиальны и количество строк не превышает 3, мы можем поместить этот код в блок if.

Нет побочных эффектов

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

Отдельные команды и запросы

Давайте посмотрим, что дядя Боб говорит о разделении команд и запросов:

По словам дяди Боба:

Функции должны либо что-то делать, либо отвечать на что-то, но не то и другое одновременно. Либо ваша функция должна изменить состояние объекта, либо она должна вернуть некоторую информацию об этом объекте. Выполнение обоих часто приводит к путанице.

Приведенный выше код является примером программы, не соответствующей шаблону CQS. Метод AddItem возвращает объект Item, что нарушает правило, согласно которому команды ничего не должны возвращать. Точно так же запрос GetItemById обновляет свойство Count, что означает, что эта операция не только возвращает информацию об объекте, но и обновляет его состояние, тем самым нарушая правило, согласно которому запрос должен возвращать только информацию.

Обрабатывайте ошибки правильно

Возврат кодов ошибок является прямым нарушением парадигмы Command Query Separation. Вместо этого мы должны использовать блоки try-catch и генерировать исключения для обработки ошибок во время выполнения. Эта практика предотвращает глубоко вложенные структуры и позволяет нам отделить код обработки ошибок от кода счастливого пути.

Первое правило написания хорошей функции состоит в том, что функция должна делать одну и только одну вещь. Обработка ошибок — это тоже одно. Следовательно, функции, обрабатывающие ошибки, не должны делать ничего другого. Если ключевое слово try существует в функции, оно должно быть первым словом в функции, и после блоков catch/finally не должно быть ничего.

Удалить мертвый код

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

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

Самый быстрый способ обнаружить мертвый код — использовать хорошую IDE. .NET предоставляет функцию добавления пользовательского файла EditorConfig в вашу кодовую базу для обеспечения последовательной практики написания кода. В Borda мы используем эту функцию для выдачи предупреждений в случае обнаружения в проекте какого-либо мертвого кода.

Заключение

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

Одно можно сказать наверняка: написание чистых функций — это не навык, который можно приобрести за одну ночь. Это навык, который необходимо развивать, помня об этих принципах и используя их всякий раз, когда вы пишете код. Ничего страшного, если в итоге вы напишете длинные и запутанные функции. Только не забудьте вернуться к функциям и отрефакторить их, как только они заработают!

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