Избегайте неправильной абстракции с помощью инкапсуляции и композиции

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

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

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

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

В некоторых случаях это может быть приемлемым решением, например, библиотечный код для четко определенных, маловероятных изменений и чисто технических проблем. Однако, если мы наивно примем тот же подход к коду бизнес-логики, этот код станет катастрофой для обслуживания, по иронии судьбы :(

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

Например, нередко можно увидеть следующий сценарий:

1. Пишем функцию, которая что-то делает:

function doSomething() {
  // ... the logic to do something ...
}

2. Мы видим новое требование, которое требует похожей функции, но не совсем такой же, поэтому мы пытаемся избежать дублирования, повторно используя существующую реализацию с небольшими изменениями:

function doSomething(flag: boolean) {
  if (flag) {
    // ... the logic to do something for scenario A ...
  } else {
    // ... the logic to do something for scenario B...
  }
}

3. В другой раз мы видим похожие потребности, и мы снова пытаемся настроить функцию:

function doSomething(flag1: boolean, flag2: boolean) {
  if (flag1) {
    if (flag2) {
      // ... the logic to do something for scenario A ...
    } else {
      // ... the logic to do something for scenario B ...
    }
  } else {
    if (flag2) {
      // ... the logic to do something for scenario C ...
    } else {
      // ... the logic to do something for scenario D ...
    }
  }
}

Видишь, что грядет? Общая функция быстро раздувается. В текущем случае у нас есть только 3 варианта использования, но нам нужно поддерживать 4 сценария; если мы добавим еще один вариант использования с аргументом, у нас будет 8 сценариев для поддержки всего 4 вариантов использования — какой сюрприз!

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

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

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

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

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

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

Пример псевдокода выдуман, а сценарий не выдуман — мы это часто видим, мы это видим много, и я не единственный, кто пытается с этим бороться.

Санди Мец выступила на RailsConf, а затем написала статью «Неправильная абстракция», в которой утверждала, что дублирование намного дешевле, чем неправильная абстракция и предпочитайте дублирование неправильной абстракции [1]. Кент С. Доддс также написал на эту тему и выступил с речью на React Summit — Программирование AHA, заявив, что мы должны сначала оптимизировать для изменений [2]. Многие другие также высказались против распространенных ошибок, допускаемых при повторном использовании кода [3][4][5].

Теперь мы понимаем, что делиться и повторно использовать неправильную абстракцию вредно. Что мы должны делать вместо этого? Достаточно ли просто «предпочесть дублирование» и ждать, пока проблемы, вызванные дублированием, не станут очевидными?

У меня есть собственное мнение на этот счет: вместо того, чтобы пытаться повторно использовать высокоуровневые функции или делать их «повторно используемыми», мы должны сосредоточиться на деталях более низкого уровня и определить, что можно инкапсулировать. Затем мы можем использовать эти инкапсуляции для повторной сборки или перекомпоновки вариантов использования высокого уровня.

Чтобы продемонстрировать процесс, давайте предположим, что мы наивно стремимся создать этот код для повторного использования:

function doBigThing(flag: boolean) {
  // ... some code to do small thing A ...

  if (flag) {
    // ... some code to do small thing B ...
  } else {
    // ... some code to do small thing C ...
  }
}

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

function doSmallThingA() {
  // ... some code to do small thing A ...
}
function doSmallThingB() {
  // ... some code to do small thing B ...
}
function doSmallThingC() {
  // ... some code to do small thing C ...
}

function doBigThing(flag: boolean) {
  doSmallThingA();

  if (flag) {
    doSmallThingB();
  } else {
    doSmallThingC();
  }
}

Второй шаг — разбить высокоуровневую функцию doBigThing на более конкретные версии, не имеющие условной логики:

function doBigThing() {
  doSmallThingA();
  doSmallThingC();
}

function doBigThingWithFlag() {
  doSmallThingA();
  doSmallThingB();
}

Здесь мы видим закономерность:

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

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

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

Кстати, в качестве примеров я использовал простые функции JavaScript/TypeScript, но этот шаблон применим и к компонентам React/JSX. Если есть тенденция сделать компонент повторно используемым путем добавления дополнительных конфигураций, мы должны сделать шаг назад и подумать, не лучше ли не делиться компонентом, а вместо этого инкапсулировать его содержимое в виде подкомпонентов и объединять эти подкомпоненты для аналогичных высокоуровневых вариантов использования.

И последнее, но не менее важное: вот практическое предложение, как избежать неправильной абстракции для повторного использования кода в повседневной работе:

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

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

[1] https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
[2] https://kentcdodds.com/blog/aha-programming
[3] https://kevlinhenney.medium.com/simplicity-before-generality-use-before-reuse-722a8f967eb9
[4] https://www.allankelly.net/archives/ 772/reuse-myth-can-you-allow-reusable-code/
[5] https://udidahan.com/2009/06/07/the-fallacy-of-reuse/

Создавайте приложения с повторно используемыми компонентами, такими как Lego.

Инструмент с открытым исходным кодом Bit помогает более чем 250 000 разработчиков создавать приложения с компонентами.

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

Подробнее

Разделите приложения на компоненты, чтобы упростить разработку приложений, и наслаждайтесь наилучшими возможностями для рабочих процессов, которые вы хотите:

Микро-интерфейсы

Система дизайна

Совместное использование кода и повторное использование

Монорепо

Узнать больше