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

Кто из программистов JavaScript не знает об объектно-ориентированном (ООП) или функциональном программировании (ФП)?! Но вот слышали ли вы об аспектно-ориентированном (АОП)? 

Что самое интересное, АОП можно без труда сочетать с ООП или ФП по аналогии с их комплексным применением в JavaScript. Поэтому целесообразно понять, в чем суть этой парадигмы и как она может пригодиться разработчикам JavaScript.

Краткое знакомство с АОП 

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

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

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

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

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

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

  • Аспекты (что)  —  это поведение, которое вы намерены внедрить в целевой код. В контексте JavaScript под ними понимаются функции, инкапсулирующие дополнительное поведение. 
  • Советы (когда) определяют желаемое время выполнения аспекта.Они конкретизируют общие моменты, в которые должен выполняться код аспекта, например “до”, “после”, “с начала и до конца”, “при выбросе исключения” и т. д. Советы поочередно указывают на временную точку выполнения кода. В случае когда обозначено время, следующее за выполнением кода, аспекты перехватят возвращаемое значение и при необходимости его перепишут.  
  • Срезы (где) указывают место для внедрения аспекта в целевом коде. Теоретически точка выполнения нужного кода может находиться в любом участке целевого кода. Однако теория расходится с практикой, поэтому можно указать условия следующим образом: “все методы объекта” или “только этот конкретный метод”, а можно придумать и более оригинальную формулировку  —  “все методы, начинающиеся с get_”.

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

Базовая реализация 

Рассмотрим наглядный пример реализации метода 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("Tryin 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)

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

В завершении этого примера приведем полученный результат: 

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

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

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

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

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

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

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

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

Как бы не избито это ни звучало, но:  

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

Тогда как с АОП приходит требование иметь качественные знания в разработке ПО. 

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

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

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

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Fernando Doglio: Aspect-Oriented Programming in JavaScript