Зарабатывая на жизнь написанием кода в течение многих лет, я обнаружил кое-что интересное: то, что делают программисты, — это не что иное, как метание между абстрактным и конкретным. Они либо извлекают что-то, чтобы сделать их более общими (чтобы их можно было широко использовать), либо используют экземпляр одной абстракции для решения конкретной проблемы. Другими словами, они абстрагируют код для конкретных применений (хотя иногда они заходят слишком далеко).

Следует отметить, что ни один инженер-программист, прошедший этическую подготовку, никогда не согласится написать DestroyBaghdad процедуру. Вместо этого базовая профессиональная этика потребовала бы, чтобы он написал DestroyCity процедуру, параметром которой мог быть указан Багдад.

— Натаниэль Боренштейн

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

Хороший код, плохой код?

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

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

Функция форматирования строк

Давайте представим небольшую функцию на странице: нам нужно отформатировать данный заголовок определенным образом — в верхнем регистре. Например, когда заголовок this is a string, нам нужно преобразовать его как:

('Title | ' + 'this is a string').toUpperCase()

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

const formatTitle = (str: string) => `Title | ${str.toUpperCase()}`

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

Таким образом, код стал намного более гибким. Вы можете сделать его либо верхним, либо нижним, как вам нравится. Однако, если вы хотите преобразовать строку другими способами (например, сделать только заглавной первую букву строки), нам нужно еще раз расширить параметр:

По мере роста логики преобразования нам также приходится размещать множество ветвей if-else внутри formatTitle, пока мы не поймем, что здесь нужно больше абстракции. В частности, нам нужна вещь, которая может делать преобразование здесь. Другими словами, upper, lower и capitalise являются экземплярами чего-то, и мы дадим этому объекту имя здесь: transformer.

С помощью этой абстракции мы разделяем format и transform и можем передать любой экземпляр преобразователя, чтобы получить разные результаты.

Например, передача x.toUpperCase для преобразования строки в верхний регистр:

const title = formatTitle('this is a title', (x) => x.toUpperCase())

И передача x.toLowerCase для преобразования строки в нижний регистр:

const title = formatTitle('this is a title', (x) => x.toLowerCase())

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

Например, если мы хотим сделать жал в формате заголовка (This Is A Title Format)

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

Обратите внимание, как мы постепенно извлекаем transform из formatTitle и упрощаем замену другой реализации. По сути, именно так вы занимаетесь проектированием программного обеспечения в целом.

P.S. Упражнение: попробуйте изменить formatTitle, чтобы сделать префикс легко заменяемым, например, пользователь может указать что угодно в качестве префикса.

Еще один пример реакции

Допустим, у нас есть аналогичный случай в React:

Первый компонент lineTitle в основном жестко запрограммирован и не может использоваться повторно (если только вам не нужен точный заголовок «Это заголовок»), а второй принимает один параметр, что намного лучше. Чтобы сделать причудливое преобразование, у нас есть второй параметр:

На самом деле, мы могли бы сделать больше с React. Например, если мы хотим, чтобы Title было не h1, а h3, мы можем извлечь здесь новую концепцию, функцию, которая принимает строку и возвращает ReactNode:

И используйте его так же, как обычный компонент. Обратите внимание, как вы можете вернуть узел JSX, который открывает совершенно новый мир для того, что здесь может сделать render. Вы даже можете вернуть header>h3.className здесь.

На самом деле, в React можно даже упростить render до children и не нужно передавать функцию в props:

И тогда ваш код немного упростится, и вы сможете передавать буквально все, что вам нравится в детстве:

И теперь, если мы соберем их все вместе, вы увидите, насколько мощным является этот процесс абстрагирования.

Обратите внимание на шаблон: попробуйте извлечь концепцию из нескольких экземпляров: h2 или h3 — все это разные экземпляры heading, и как только мы извлекли heading, мы заметим, что мы не ограничены heading, а более широкая концепция: component(ReactNode ), а затем render props или children имеют больше смысла.

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

Краткое содержание

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

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

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