Код
#статьи

Аспектно-ориентированное программирование в JavaScript

Нет, мы не опечатались — оно существует. А умеет такое, с чем совладают немногие разрабы: рассказываем, что — и как не хватить с этим лиха.

скриншот из игры necromunda: hired gun / streum on studio

Фернандо Дольо

(Fernando Doglio)


об авторе

Топовый автор Medium. Пишет о технологиях, об образе жизни, личном опыте и многом другом.


Кто не знает про объектно-ориентированное программирование, да и функциональное явно на слуху, а вот об аспектно-ориентированном вы когда-нибудь слышали?

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

Что самое крутое, АОП легко сочетается как с функциональным, так и с объектно-ориентированным программированием (ООП).

Что ж, разберёмся, что такое АОП и чем оно полезно JavaScript-разработчикам.

Знакомство с АОП

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

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

Разберём на примере

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

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

Аспекты, срезы, советы, или Что, где и когда

Познакомимся с тремя ключевыми понятиями АОП. Они прояснят суть парадигмы и помогут изучать её дальше.

  • Аспекты (aspects, «что») — это добавочное поведение, которое нужно включить в целевой код. В контексте JavaScript это функции, которые инкапсулируют нужное нам поведение.
  • Срезы (pointcuts, «где») указывают место в целевом коде, куда следует внедрить аспект. Теоретически оно может быть любым, но на практике такое выходит не всегда. И всё же вы можете указывать условия вроде «все методы моего объекта», или «только этот конкретный метод», или даже что-то типа «все методы, которые начинаются с get_».
  • Советы (advice, «когда») определяют момент, когда должен выполняться код аспекта, например:
  • before (перед вызовом метода),
  • after (после возврата в точку вызова),
  • around (до вызова метода и при возврате из него),
  • whenThrowing (когда метод вызывает исключение) и так далее.

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

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

Закрепим теорию практикой.

Пример использования

Надеюсь, вам, как и мне, легче учиться на наглядных примерах. Поэтому вот вам реализация метода inject, который добавляет поведение на основе АОП. Убедитесь, как это просто и удобно.

// Вспомогательная функция для вызова всех методов объекта
const getMethods = (obj) => Object.getOwnPropertyNames(Object.getPrototypeOf(obj)).filter(item => typeof obj[item] === 'function')

// Заменяем оригинальный метод нашей функцией, которая вызовет наш аспект, когда укажет совет
function replaceMethod(target, methodName, aspect, advice) {
    const originalCode = target[methodName]
    target[methodName] = (...args) => {
        if(["before", "around"].includes(advice)) {
            aspect.apply(target, args)
        }
        const returnedValue = originalCode.apply(target, args)
        if(["after", "around"].includes(advice)) {
            aspect.apply(target, args)
        }
        if("afterReturning" == advice) {
            return aspect.apply(target, [returnedValue])
        } else {
            return returnedValue
        }
    }
}

module.exports = {
    // Экспорт главного метода: внедряем аспект где и когда нужно
    inject: function(target, aspect, advice, pointcut, method = null) {
        if(pointcut == "method") {
            if(method != null) {
                replaceMethod(target, method, aspect, advice)    
            } else {
                throw new Error("Trying to add an aspect to a method, but no method specified")
            }
        }
        if(pointcut == "methods") {
            const methods = getMethods(target)
            methods.forEach( m => {
                replaceMethod(target, m, aspect, advice)
            })
        }
    }   
}

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

А сперва обратите внимание на реализацию replaceMethod. Вот где творится вся магия. Именно здесь создаётся новая функция, именно тут мы решаем, когда вызывать аспект и что делать со значением, которое он возвращает.

Теперь покажем, как использовать нашу новую библиотеку:

const AOP = require("./aop.js")

class MyBussinessLogic {

    add(a, b) {
        console.log("Calling add")
        return a + b
    }

    concat(a, b) {
        console.log("Calling concat")
        return a + b
    }

    power(a, b) {
        console.log("Calling power")
        return a ** b
    }
}

const o = new MyBussinessLogic()

function loggingAspect(...args) {
    console.log("== Calling the logger function ==")
    console.log("Arguments received: " + args)
}

function printTypeOfReturnedValueAspect(value) {
    console.log("Returned type: " + typeof value)
}

AOP.inject(o, loggingAspect, "before", "methods")
AOP.inject(o, printTypeOfReturnedValueAspect, "afterReturning", "methods")

o.add(2,2)
o.concat("hello", "goodbye")
o.power(2, 3)

Ничего сверхсложного: один базовый объект с тремя методами. Мы внедрили пару общих для всех них аспектов: один выводит в лог полученные атрибуты, а другой анализирует возвращаемое значение и выводит его тип.

Два аспекта — две строки кода (вместо шести, которые понадобились бы без АОП).

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

Преимущества аспектно-ориентированного программирования

Разобравшись, что такое АОП и на что оно способно, вы наверняка догадываетесь, почему оно вызывает такой интерес.

Пробежимся по его главным преимуществам.

1. Инкапсуляция сквозной функциональности

Я вообще большой поклонник инкапсуляции — благодаря ей код легче читать и поддерживать, а также использовать повторно во всём проекте.

2. Гибкость логики

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

3. Повторное использование кода

Аспекты — это, по сути, компоненты, небольшие независимые фрагменты кода, которые могут работать где угодно. Если они правильно написаны, то их легко применять в других проектах.

Главная проблема АОП

Ничто не идеально, и АОП тоже есть за что поругать.

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

И отчасти противники АОП правы. Эта парадигма позволяет добавить в существующие объекты нехарактерное им поведение или даже заменить всю их логику. Конечно, АОП появилось совсем не для этого, и в примере выше я показывал вовсе не такие «возможности».

Однако АОП позволяет сделать всё, что вы хотите. А такая вседозволенность, помноженная на неопытность разработчика, открывает ящик Пандоры.

Банально это или нет, повторю легендарную фразу из комиксов:

С великой силой приходит и большая ответственность.

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

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

Подытожим

Аспектно-ориентированное программирование отлично дополняет ООП. Благодаря динамической природе JavaScript применять АОП с ним очень легко, как я и показал на примерах.

АОП позволяет выделять код в модули, разделять логику и переиспользовать разработки в других проектах.

Конечно, если применять АОП неумело, то можно всё запутать, но при правильном подходе оно позволяет создавать более чистый и понятный код.


Изучайте IT на практике — бесплатно

Курсы за 2990 0 р.

Я не знаю, с чего начать
Научитесь: Профессия Python-разработчик Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована