Как не быть тем парнем, а писать функции лучше

Понятные и поддерживаемые функции – не миф. Используйте эти 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 [];
};

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

Заключение

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

«В программировании элегантность – не дополнительная роскошь, а качество, которое определяет успех и неудачу».

Не будь тем парнем, пиши функции лучше.

Оригинал

А каким принципам написания функций следуете вы?

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
30 июня 2018

Шаблоны проектирования в Python: для стильного кода

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