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

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

Обновление от 27 февраля. Я опубликовал соответствующее видео на случай, если вы предпочитаете другой формат:

Пусть машина делает тяжелую работу.

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

«Время программиста стоит дорого; экономьте его, а не машинное время».
— Эрик С. Рэймонд.

Я предпочитаю, чтобы код был чистым и интуитивно понятным, даже если он не идеален с точки зрения производительности. Я приведу вам быстрый пример здесь. Если мы стремимся к высокой производительности, мы можем выполнить итерацию коллекции за один раунд, чтобы завершить все необходимые операции. С другой стороны, объединение этих различных операций вместе с несколькими API-интерфейсами коллекций (например, data.filter().map().reduce()) и с помощью небольших, но специализированных функций может сделать код более читабельным. Что бы вы тогда выбрали?

Увеличим немного

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

[
  {
    "id": 1,
    "name": "Desdemona",
    "role": "Engineer",
    "experience": 1,
    "location": null
  },
  {
    "id": 2,
    "name": "Paloma",
    "role": "Construction Foreman",
    "experience": 18,
    "location": "Hali"
  },
  //...
]

Теперь предположим, что мне нужно выполнить две задачи:

  • Найдите всех вакансий «Инженер», у которых более трех лет опыта и
  • Если местоположение равно null, используйте резервное значение «Н/Д», которое будет использоваться в компоненте пользовательского интерфейса.

У меня есть два варианта: в одном цикле for я ввожу всю логику и получаю результат. Или я могу разделить процесс на два отдельных шага, а затем соединить их с помощью API сбора.

Функция «быстрого» преобразования

Мы можем выполнить задачу за один раунд довольно просто, как показано ниже:

const transformUsersPerformant = (users: User[]) => {
  const results = [];

  for (let i = 0; i < users.length; i++) {
    const user = users[i];
    if (user.experience > 3 && user.role === "Engineer") {
      results.push({
        ...user,
        location: user.location === null ? "N/A" : user.location,
      });
    }
  }

  return results;
};

Это работает нормально. И когда users представляет собой большой набор данных, это может быть лучшим вариантом. С другой стороны, я бы реорганизовал код во что-то более абстрактное. Обратите внимание, что мое предположение состоит в том, что список users не является большим набором данных (например, менее 1 тыс. элементов).

Более медленная, но чистая версия

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

const isNoLocation = (user: User) => {
  return user.location === null;
};

const fillBlankLocationForUser = (user: User) => {
  if(isNoLocation(user)) {
    return {...user, location: 'N/A'}
  }
  return user;
}

const isSeniorEngineer = (user: User) =>
  user.experience > 3 && user.role === "Engineer";

const transformUsersAbstract = (users: User[]) =>
  users.filter(isSeniorEngineer).map(fillBlankLocationForUser);

С точки зрения производительности этот код «плохой». Он повторяет users дважды (если вы поменяете местами вызовы map и filter, чтобы получить тот же результат, но еще медленнее). У него много «ненужных» вызовов функций (поэтому больше стека push/pops и, в конечном счете, медленнее).

Тем не менее, я думаю, что новый подход лучше, по следующим причинам:

  • isSeniorEngineer намного чище, чем сырой user.role === '' && user.experience > 3
  • Критерии isSeniorEngineer могут быть изменены, и нам не нужно гадать, что определяет старший инженер, а читать буквально.
  • fillBlankLocation и isNoLocation можно повторно использовать в других местах

Итак, какова скорость однораундовой итерации?

Я провел простой тест для этих двух подходов с 1000 пользовательских объектов (117 КБ). Производительность, ну, примерно такая же (я использовал performance.now(), так что, кстати, результат в миллисекундах).

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

Что с ремонтопригодностью

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

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

users.filter(isSeniorEngineer).map(fillBlankLocationForUser).reduce(summarise)

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

for (let i = 0; i < users.length; i++) {
  const user = users[i];
  if (user.experience > 3 && user.role === "Engineer") {
    results.push({
      ...user,
      location: user.location === null ? "N/A" : user.location,
    });
  }
  //...
}

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

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

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

Но производительность тоже важна, верно?

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

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

Преждевременная оптимизация — корень всех зол.
— Дональд Кнут

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

Заключение

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

Рекомендации

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

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

Надеюсь, вам понравилось это читать. Если вы хотите поддержать меня как писателя, рассмотрите возможность подписки стать участником Medium. Всего 5 долларов в месяц, и вы получаете неограниченный доступ ко всем моим статьям на Medium — а также ко всем статьям упомянутых авторов и всех остальных!