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

Функциональное программирование предлагает элегантные решения в виде двух техник: деметодизации (demethodizing) и методизации (methodizing).

В статье рассмотрим обе техники и на простых примерах, покажем, как demethodizing и methodizing могут расширить возможности работы с кодом. Для демонстрации этих подходов будем использовать TypeScript.

Demethodizing

Demethodizing — это техника функционального программирования, позволяющая извлекать метод из объекта и преобразовывать его в независимую функцию.

Мы можем реализовать функцию demethodize() тремя равнозначными способами: используя методы apply(), call(), или bind().

Реализация с применением метода apply():

const demethodize =
<T extends (obj: any, ...args: any[]) => any>(fn: T) =>
(obj: any, ...args: Parameters<T>): ReturnType<T> =>
fn.apply(obj, args);

Реализация с применением метода call():

const demethodize =
<T extends (obj: any, ...args: any[]) => any>(fn: T) =>
(obj: any, ...args: Parameters<T>): ReturnType<T> =>
fn.call(obj, ...args);

Функция demethodize() принимает объект в качестве первого аргумента, остальные аргументы (...args) — это фактические параметры, которые передаются вызываемому методу.

Реализация с применением метода bind():

const demethodize =
<T extends (this: any, ...args: any[]) => any>(fn: T) =>
(thisArg: any, ...args: Parameters<T>): ReturnType<T> =>
fn.bind(thisArg, ...args)();

Функция demethodize(), реализованная с помощью bind(), принимает в качестве первого аргумента функцию fn, которая ожидает контекст this и аргументы.

Рассмотрим, как demethodize() может быть использован на практике.

Предположим, что есть коллекция элементов, например, NodeList, которую получили с помощью document.querySelectorAll. В современных браузерах мы могли бы преобразовать этот объект в массив с помощью Array.from() и затем применить необходимый нам метод массива, например метод map(). Однако, если нужно поддерживать старые браузеры, которые не поддерживают Array.from(), этот подход становится неприменимым.

Вместо этого можно использовать demethodize(), чтобы преобразовать метод массива в независимую функцию, которую затем можно применить к NodeList. Это обеспечит возможность использования метода массива, минуя необходимость преобразования NodeList в массив.

const map = demethodize(Array.prototype.map); 
// Преобразуем метод Array.prototype.map в независимую функцию

const paragraphs = document.querySelectorAll(‘p');
// Получаем все параграфы 
                                          
const paragraphTexts = map(paragraphs, p => p.textContent); 
// Извлекаем текст из каждого параграфа 

В приведенном выше примере функция demethodize() позволяет преобразовать метод Array.prototype.map и применять его к NodeList напрямую. Т.о., мы можем эффективно обрабатывать коллекции элементов и извлекать из них информацию, даже если встроенные методы преобразования коллекций недоступны.

Methodizing

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

Methodizing — это техника функционального программирования, которая позволяет добавлять функцию в объект в качестве метода, обеспечивая доступ функции к контексту объекта.

Напишем реализацию функции methodize():

const methodize = <
  T extends any[],
  O extends { prototype: { [key: string]: any } },
  F extends (arg: any, ...args: T) => any
>(obj: O, fn: F) =>
  (obj.prototype[fn.name] = function (
    this: Parameters<F>[0],
    ...args: T
  ): ReturnType<F> {
    return fn(this, ...args);
  });

T — обобщённый тип параметров, которые будут передаваться в методизированную функцию.
O — тип объекта, в прототип которого добавляется новый метод.
F — функция, которую мы методизируем. Первый аргумент (arg) будет являться контекстом вызова (this). Остальные аргументы (если таковые имеются) имеют тип T.

Рассмотрим использование функции methodize() на иллюстративном примере добавления функции reverse() к объекту String.

function reverse(this: string): string {
  return this.split('').reverse().join('');
} 
// Функция reverse принимает строку в качестве аргумента 
// и возвращает новую строку с символами в обратном порядке


methodize(String, reverse); 
// Добавление метода к объекту String


'METHODIZING'.reverse(); // ‘GNIZIDOHTEM’

О преимуществах и недостатках

Demethodizing и methodizing — это мощные техники, которые при грамотном использовании способны улучшить читабельность и структуру кода. Эффективность этих техник зависит от контекста и соответствия задачам проекта. Рассмотрим, в каких ситуациях эти подходы приносят пользу, а когда их применение может оказаться неуместным.

Преимущества

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

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

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

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

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

Недостатки

В высоконагруженных системах, где важна каждая миллисекунда, чрезмерное использование функции demethodize() может привести к потере производительности, т.к. для реализации этой функции применяется один из трех методов: call(), apply() или bind(), которые требуют дополнительных вычислительных ресурсов для изменения контекста выполнения функции.

При использовании методов call() и apply(), функция вызывается с явно указанным контекстом и аргументами. Это требует дополнительных вычислений для установки нового контекста выполнения и обработки передаваемых аргументов.

При использовании bind() создается новая функция, которая сохраняет переданный контекст и аргументы. Создание новой функции требует выделения памяти и увеличивает время выполнения.

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

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

Заключение

В статье были рассмотрены две техники функционального программирования: деметодизация (demethodizing) и методизация (methodizing), а также их преимущества и недостатки.

Demethodizing позволяет преобразовать метод объекта в независимую функцию. Были рассмотрены три способа реализации функции demethodize() с помощью методов apply(), call() и bind(). Пример использования demethodize() показал, как можно эффективно работать с коллекциями элементов, такими как NodeList, минуя необходимость их преобразования в массив.

Methodizing позволяет интегрировать функцию в объект в качестве метода, что дает функции доступ к внутреннему состоянию объекта. В качестве иллюстративного примера была использована функция methodize(), которая добавляет метод reverse() к прототипу объекта String.

Demethodizing и Methodizing предоставляют дополнительные возможности для работы с функциями и объектами.

Комментарии (9)


  1. artptr86
    26.08.2024 13:24
    +2

    Array.prototype.map.call(paragraphs, p => p.textContent)

    А расширять прототипы стандартных объектов крайне не рекомендуется.


    1. Serj_Pashnin Автор
      26.08.2024 13:24

      Приведенный в статье пример с объектом String является исключительно иллюстративным.


    1. Serj_Pashnin Автор
      26.08.2024 13:24
      +2

      Что же касается вашего замечания, в примере кода использовался существующий метод массива map, вызванный через Function.prototype.call для объекта NodeList. Этот подход позволяет эффективно использовать методы массива на структурах, похожих на массивы, без изменения их прототипов. Такое применение не вносит изменений в прототипы и является безопасным.


      1. artptr86
        26.08.2024 13:24

        Вам ChatGPT помогает не только статьи писать, но и комментарии? Расширение прототипа вы делаете в методе methodize(), а Array.prototype.call вообще не существует — всё это предложение по стилю и галлюцинациям слишком похоже на ChatGPT.


        1. Serj_Pashnin Автор
          26.08.2024 13:24
          +2

          Про расширение объекта String c помощью функции methodize() ответил выше.

          В коде, который приведен в статье указано: obj.prototype[fn.name]


  1. mint_K
    26.08.2024 13:24
    +1

    по содержанию статья хорошая.
    По стилю, как по мне - очень официозная, особенно для хабр, но это восприятию не мешает.

    хз как на счет Methodizing (как по мне проще явно метод в нужный класс добавить) , а вот Demethodizing - пригодится.

    Возьму на заметку. Спасибо!


    1. Serj_Pashnin Автор
      26.08.2024 13:24

      Спасибо за ваш комментарий!
      Мне очень приятно, что содержание статьи вам понравилось.

      Формальный стиль написания статьи выбран мной целенаправленно.

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


  1. victor-homyakov
    26.08.2024 13:24
    +1

    Как сказал бы Деми Мурыч, "никто не читает спецификацию языка". Статья кое о чём важном умалчивает: реализация метода должна быть изначально пригодна для того, что в этой статье называется "деметодизация".

    Например, многие методы из Array.prototype используют для работы только поле length и доступ к элементам по индексу. Именно поэтому они могут быть применены через bind, call или apply к любому массивоподобному объекту (то есть объекту, у которого есть length и доступ по индексу).

    А какие методы стандартных объектов подходят для такого использования, и какие нет - указано в спецификации языка. Это называется термином "generic". Например:

    И вот было бы неплохо, если бы автор дал какие-то нетривиальные примеры использования generic методов, а не повторяющееся уже десятилетиями применение методов массива к массивоподобным объектам. Да и с массивоподобными объектами можно поступать намного проще - применил slice(), получил настоящий массив и делай с ним что угодно, и не надо "деметодизировать" все остальные методы работы с массивами.


    1. Serj_Pashnin Автор
      26.08.2024 13:24

      Спасибо за комментарий!

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

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