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

Если вы работаете в сфере разработки программного обеспечения больше пяти минут, то наверняка слышали священную мантру, разносящуюся по конференц-залам и обзорам кода: «Не изобретай колесо!». Её произносят с тем же благоговением, которое обычно приберегают для древних мудростей, часто сопровождая понимающим кивком и быстрой установкой очередной зависимости весом в 47 МБ для центрирования div.

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

Заговор зоны комфорта

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

Подумайте об этом: когда в последний раз вы задавались вопросом, стоило ли увеличение размера бандла на 200 КБ из-за той утилитной библиотеки, которую вы подключили для одной функции? Или когда вы потратили три часа на отладку стороннего компонента, в то время как собственное решение можно было бы написать за два часа и оно бы работало именно так, как вам нужно?

graph TD A[Требуются новые функции] --> B{Проверить npm} B --> C[Найдено 47 библиотек] C --> D[Выбрать самую популярную] D --> E[Установить 23 зависимости] E --> F[Размер бандла +500 КБ] F --> G[Функция почти работает] G --> H[Потратить 2 дня на отладку] H --> I[Рассмотреть собственное решение] I --> J[Слишком поздно, выпустить]

Реальность такова, что API браузеров претерпели значительные изменения. У нас есть невероятные инструменты, встроенные в современные браузеры, которые многие библиотеки JavaScript полностью игнорируют. И всё же мы продолжаем загружать интернет по одной упаковке за раз, потому что, ну, «не изобретай колесо».

Парадокс обучения

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

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

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

Когда переосмысление имеет смысл

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

Критически важные для производительности приложения

Если вы создаёте платформу для торговли в реальном времени, вы, вероятно, не захотите включать библиотеку анимации весом в 50 КБ только для того, чтобы ваши кнопки подпрыгивали. Иногда несколько строк пользовательского CSS сделают работу лучше, быстрее и с меньшими накладными расходами.

// Вместо импорта всего lodash для одной функции
import { debounce } from 'lodash'; // +50КБ
// Рассмотрим эту лёгкую альтернативу
function debounce(func, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}
// +0,1 КБ, и вы точно знаете, что оно делает

Высокоспецифичная бизнес-логика

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

Обучение и развитие навыков

Хотите понять, как работает управление состоянием? Создайте мини-Redux. Любопытно узнать о виртуальном DOM? Создайте простую версию. Вы получите понимание, которого не даст ни один объём документации.

Долгосрочное обслуживание

Иногда поддерживать собственное решение из 100 строк проще, чем следить за изменениями в библиотеке каждые шесть месяцев. Особенно если вы используете только 2 % функций библиотеки.

Пошаговый подход к разумному переосмыслению

Вот фреймворк, который я использую при принятии решения о том, создавать или покупать:

Шаг 1: Определите свои точные потребности

Будьте конкретны. Запишите, что именно вы хотите, чтобы решение делало. Не то, что было бы неплохо иметь, не то, что делает популярная библиотека, — а то, что нужно вам.

// Плохо: «Мне нужна библиотека для работы с датами»
// Хорошо: «Мне нужно форматировать даты в формате ISO и вычислять различия в рабочих днях для планирования с учётом часовых поясов»

Шаг 2: Оцените собственное решение

Сколько времени потребуется вам, чтобы создать именно то, что вам нужно? Будьте честны, добавьте запас на отладку и краевые случаи.

Шаг 3: Оцените существующие решения

Посмотрите на популярные варианты. Проверьте их:

  • размер бандла и зависимости;
  • область применения API (сколько вы на самом деле будете использовать);
  • статус обслуживания и здоровье сообщества;
  • совместимость лицензий;
  • характеристики производительности.

Шаг 4: Рассчитайте общую стоимость владения

graph LR A[Время первоначальной настройки] --> B[Кривая обучения] B --> C[Сложность интеграции] C --> D[Текущие обновления] D --> E[Время поддержки/отладки] E --> F[Риск миграции] F --> G[Общая стоимость] H[Собственная разработка] --> I[Время тестирования] I --> J[Документация] J --> K[Техническое обслуживание] K --> L[Передача знаний команде] L --> M[Общая стоимость]

Шаг 5: Примите решение

Если победит собственное решение, создайте его. Если победит существующее решение, используйте его. Но принимайте это решение осознанно, а не по умолчанию.

Реальные примеры: когда я выбрал переосмысление

Позвольте поделиться несколькими боевыми историями, когда изобретение колеса заново окупилось:

Случай с раздутым валидатором форм

Я однажды работал над проектом, где существующая библиотека для валидации форм была сжата до 180 КБ. Наши потребности были простыми: проверить формат электронной почты и убедиться, что обязательные поля не пустые. Собственное решение:

const validators = {
  required: (value) => value.trim() !== '',
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};
function validateForm(formData, rules) {
  const errors = {};
  Object.keys(rules).forEach(field => {
    const value = formData[field] || '';
    const fieldRules = rules[field];
    fieldRules.forEach(rule => {
      if (!validators[rule](value)) {
        errors[field] = errors[field] || [];
        errors[field].push(`${field} ${rule} validation failed`);
      }
    });
  });
  return {
    isValid: Object.keys(errors).length === 0,
    errors
  };
}
// Использование
const result = validateForm(
  { email: '[email protected]', name: '' },
  { email: ['required', 'email'], name: ['required'] }
);

Результат: 2 КБ вместо 180 КБ, идеально подходит для наших нужд, и вся команда поняла, как это работает.

Бунт против управления состоянием

В другом проекте мы использовали сложную библиотеку для управления состоянием, чтобы просто обмениваться несколькими значениями между компонентами. Собственное решение с использованием встроенного в браузер API BroadcastChannel:

class SimpleState {
  constructor(initialState = {}) {
    this.state = initialState;
    this.channel = new BroadcastChannel('app-state');
    this.listeners = new Set();
    this.channel.addEventListener('message', (event) => {
      this.state = { ...this.state, ...event.data };
      this.notifyListeners();
    });
  }
  setState(updates) {
    this.state = { ...this.state, ...updates };
    this.channel.postMessage(updates);
    this.notifyListeners();
  }
  subscribe(listener) {
    this