Как не быть тем парнем, а писать функции лучше
Понятные и поддерживаемые функции – не миф. Используйте эти 4 главных принципа написания кода, и товарищи по команде скажут «спасибо».
У каждого разработчика найдётся история о проблемах с пониманием кода, который достался в наследство в конкретном проекте. Не обошло это и вас, верно? ;)
Так было с Федей, который подолгу рассказывал, как он раздражён и устал пялиться на бессмысленную функцию-мастодонта. На вопрос, не пытался ли он уточнить это у товарища по команде, который работал над программным обеспечением до него, он ответил с лёгким смешком: «(Имя товарища по команде) тоже без понятия. Он смотрел на код так же внимательно и сказал, что не писал этого».
После разговора с Федей программист осознал две вещи. Первым было: «Не будь тем парнем!». Значит не походи на человека, который заставил страдать Федю. Вторым было: «Пиши функции лучше». Это единственный способ не быть тем парнем.
Сто процентов, нет числа таким Фёдорам, которым приходится наследовать, осмысливать и рефакторить ужасно написанные функции в программных проектах. Правильно разработанное программное обеспечение заботится о небольших единицах (или методах), то есть функциях на микроуровне, а не только об общей функциональности на макроуровне.
Раз так часто пишем ужасные функции, пора задуматься об улучшении в этой конкретной области. Ниже приводятся некоторые руководящие принципы и подходы опытных специалистов, коллег и других рекомендуемых источников.
Введение
Определение вещей – логичное начало. Функции – запрограммированные операции. Если хочется больше слов, переходим в Google. Программные системы состоят из функций в разной степени. Возможно, разрабатываем программное обеспечение в объектно-ориентированном стиле, в котором функции живут внутри классов, составляющих систему, и влияют на состояние соответствующих классов. Или система использует функционально-ориентированную парадигму и разбита на набор взаимодействующих централизованно функций. Независимо от подхода, функции остаются, потому что не обойтись без декомпозиции решения. И на очень низком уровне разделения находим эти небольшие единицы, которые помогают достичь конкретной цели.
Функции должны быть маленькими
И всё-таки размер имеет значение.
Маленькие функции легки для чтения, понимания, тестирования и отладки. Не будем называть магическое число. Некоторые эксперты скажут не больше 15 строк, другие – не больше 25. Это, вероятно, придётся решать в каждой команде. Важно помнить причины сохранения маленького размера функций.
Удобочитаемость: функция, как правило, состоит из сигнатуры и блока кода, который выполняется при вызове или запуске функции. Чем меньше строк кода в блоке функции, тем легче прочитать и понять, что функция делает.
Понятность: меньшие функции помогают снизить вероятность отклонения от главного назначения функции. Чем более линейное назначение, тем понятнее.
Тестируемость: у коротких методов меньше вариаций, что означает, их легче тестировать.
Вот пример функции, предназначенной для проверки правильности токена:
const isTokenValid = (token: string | null): boolean => { if (!token) { return false; } try { const decodedJwt: any = decode(token); return decodedJwt.exp >= Date.now() / 1000; } catch (e) { return false; } }
Функции должны быть чистыми
Да, звучит неоднозначно. Однако речь идёт не столько о стиле кода, отступах или длине имён переменных. Речь идёт о понятности. Сможет ли Федя взглянуть на вашу функцию, понять её смысл и внести изменения, не убив дни работы?
Важно, насколько поддерживаемый будет ваш код, ведь поддерживаемый код составляет каркас поддерживаемого программного обеспечения. Для определения чистоты кода используются и другие атрибуты, которые субъективны, и решаются лично или в команде.
Функции должны быть простыми
Технический руководитель часто говорит: «Если это (функция) требует больших усилий, остановитесь и переосмыслите решение». В нашей области усилия чаще не приветствуются, потому что порождают нечто сложное.
«В разработке программного обеспечения усилия не растут линейно со сложностью – они растут в геометрической прогрессии. Поэтому легче управлять двумя наборами из четырёх сценариев в каждом, чем одним с шестью ». – Abraham Marín-Pérez
Если писать функции на основе модульности и сокращать пути выполнения, будет намного проще понять их назначение. Когда пишут сложный код, его гораздо труднее понять, и подобные недоразумения часто приводят к ошибкам.
Вот пример функции, которая проверяет, что полученный аргумент – массив строк:
const checkIfArrayOfStrings = (arrayToCheck: any): Array<string> => { if (arrayToCheck && arrayToCheck instanceof Array && arrayToCheck.length) { const arrayOfNonStringValues = arrayToCheck.filter((value: any) => { return typeof value !== 'string'; }); if (arrayOfNonStringValues && arrayOfNonStringValues.length) { return []; } return arrayToCheck; } return []; };
Функции должны быть однозадачными (без побочных эффектов)
Роберт Мартин лучше выразил это в «Чистом коде»: «Функция обещает сделать одну вещь…», и, следовательно, так и должно быть. Наличие побочных эффектов только делает код менее читаемым из-за изменений в блоке кода, которые не приводят к достижению этой конкретной цели. Функции должны быть основаны на детерминированном алгоритме – при заданных входных данных возвращать один и тот же результат.
Возьмём следующий пример: функция предназначена для получения даты и недели, где дата встречается, в виде массива с объектами даты.
const getDaysOfWeekFromGivenDate = ( date: Date | null ) => { if (date) { const startOfWeek = moment(date).startOf('isoWeek'); const weekArray = moment.weekdays(); const daysOfWeekInSelectedDate = daysOfWeek.map((d, i) => { return startOfWeek .clone() .add(i, 'd') .toDate(); }); return daysOfWeekInSelectedDate; } else { return []; } };
Говорят, что у функции, как правило, одна цель. Однако, возможно, заметили момент, когда генерируем неделю на основе двух аргументов: объекта (в данном случае объекта Moment) и дня недели (воскресенье, понедельник, вторник и т. д.). Таким образом, лучше создать новую функцию для упрощения и сделать методы более линейными относительно назначения.
Когда разделим функцию на две части, получаем следующее:
const getDaysOfWeekFromGivenDate = ( date: Date | null ) => { if (date) { const startOfWeek = moment(date).startOf('isoWeek'); const weekArray = moment.weekdays(); const daysOfWeekInSelectedDate = generateWeek( startOfWeek, weekArray ); return daysOfWeekInSelectedDate; } else { return []; } }; const generateWeek = ( startOfWeek: moment.Moment, daysOfWeek: Array<string> ): Array<Date> => { if (startOfWeek && daysOfWeek.length) { return daysOfWeek.map((d, i) => { return startOfWeek .clone() .add(i, 'd') .toDate(); }); } return []; };
В результате случайному программисту теперь проще понять смысл наших функций, создать для них контрольные примеры и изменить их при необходимости.
Заключение
Эти рекомендации – не единственные, однако закладывают крепкое основание для создания классного кода при написании функций. Кроме того, это потребует практики, обдуманного рефакторинга и взгляда со стороны (рецензий). Покажется, что нужны дополнительные усилия для создания такого рода кода, но это того стоит. Эдсгер Дейкстра, крёстный отец программирования, сказал следующее:
«В программировании элегантность – не дополнительная роскошь, а качество, которое определяет успех и неудачу».
Не будь тем парнем, пиши функции лучше.