Я получил много комментариев к своей недавней статье о рефакторинге и соответствующему видео. Я ответил на некоторые из них здесь и там, и вскоре понял, что было бы полезно собрать их в одном месте и обсудить тему немного глубже.
Есть особый комментарий о том, что рефакторинговая версия имеет слишком много абстракций и может быть менее производительной, чем исходный подход. Казалось бы, меньшая абстракция и меньшее количество слоев могут привести к более быстрой программе. Но является ли быстрая работа нашей единственной целью для обычных приложений?
Обновление от 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/pop
s и, в конечном счете, медленнее).
Тем не менее, я думаю, что новый подход лучше, по следующим причинам:
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, регистров, системных вызовов, прерываний и сигналов и т. д.
И я не видел проблем с производительностью исключительно из-за того, что в коде слишком много уровней абстракции. Я вижу код, который трудно читать и тестировать, и на изменение которого уходят дополнительные дни и недели: добавление даже небольших функций затруднено, а исправить дефект может быть еще сложнее, не нарушая других «не связанных» вещей.
Если вас интересует ремонтопригодность, я создал шпаргалку с большим количеством шаблонов и советов по этому вопросу, и вы можете распечатать ее, если хотите.
Но производительность тоже важна, верно?
Немного о производительности. Хороший дизайн делает ваш код более производительным. С хорошей абстракцией и дополнительным слоем для блокировки деталей более низкого уровня намного проще (опять же, поскольку код более читабелен) найти узкое место и соответствующим образом решить проблему с производительностью.
И я обсуждал связь между хорошим дизайном и производительностью в другой статье Кто-то сказал композиция? — проблемы с производительностью во многих случаях вызваны плохим дизайном.
Преждевременная оптимизация — корень всех зол.
— Дональд Кнут
Трудно предугадать, какая часть может вызвать проблемы с производительностью, а угадывать — не очень хорошая привычка в кодировании. Как вы видели выше, эти дополнительные абстракции не вызывают больших проблем. Поэтому мой подход заключается в том, чтобы не оптимизировать код заранее. Вместо этого вы должны установить подходящий механизм измерения и отслеживать цифры в процессе непрерывной доставки.
Заключение
В этом сообщении в блоге я взвешиваю важность чистого кода по сравнению с производительным кодом. Чистый код предпочтительнее, поскольку он обеспечивает более четкую и удобную в сопровождении кодовую базу. Я утверждаю, что уделение особого внимания производительности может привести к созданию сложных проектов, которые сложно поддерживать в долгосрочной перспективе. В конечном счете, я утверждаю, что размещение чистого кода на первом месте приводит не только к повышению производительности, но и к более простому обслуживанию в будущем.
Рекомендации
- Как написать более многоразовый код?
- Правильный способ размещения бизнес-логики в вашем React-приложении
- 8 советов по улучшению производительности вашего интерфейса
- Кто-то сказал композиция?
Я надеюсь, что вы нашли этот пост в блоге информативным и полезным. Если вам интересно углубиться в тему, я создал онлайн-курс, который охватывает все темы, обсуждаемые в этом посте, и многое другое. Этот курс разработан, чтобы быть интерактивным, увлекательным и наполнен практическими советами и методами.
Если вам понравилось чтение, пожалуйста, Подпишитесь на мою рассылку. Я еженедельно делюсь методами чистого кода и рефакторинга в блогах, книгах и видео.
Надеюсь, вам понравилось это читать. Если вы хотите поддержать меня как писателя, рассмотрите возможность подписки стать участником Medium. Всего 5 долларов в месяц, и вы получаете неограниченный доступ ко всем моим статьям на Medium — а также ко всем статьям упомянутых авторов и всех остальных!