Синтаксический сахар АОП в JavaScript

в 0:20, , рубрики: aop, javascript, Программирование, метки: ,

Зачастую бывает полезно добавить какую-нибудь дополнительную логику в код, которая собирает данные по ходу работы приложения, например подсчет количества вызовов, или обработка ошибок. Но портить существующий компактно написанный код (если конечно у вас есть такое счастье) не очень-то хочется. Решение в виде приемов АОП существует уже давно, и широко применятся. На платформах .NET и Java многие АОП фреймворки ориентируются на применение атрибутов к методам и классам. Выглядит код почти неизмененным, а в распоряжении оказывается достаточно мощный механизм расширения функциональности.

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

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

Мне ближе .NET атрибуты, да и реализация такого синтаксиса напрашивается сама собой, поэтому исходить будем из небольшого примера на C#

[AttributeName(Params)]
[AttributeName(Params), AttributeName(Params)]
public void Foo() {
…
}

Немного модифицируем код, чтобы он был валиден для JavaScript

AOP(
	[AttributeName(Params)],
	[AttributeName(Params) , AttributeName(Params)],
	function Foo() {
	…
	}
);

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

function AOP() {
    var advices = [];
    // Разбираем список параметров
    for (var i = 0; i < arguments.length; i++) {
        var arg = arguments[i];
        // Собираем все атрибуты в массивах, пока не встретим функцию,
        // для которой они все предназначены
        if (Object.prototype.toString.call( arg ) === "[object Array]") {
            for (var j = 0, advice = arg[j]; j < arg.length; j++) {
                advices.push(advice);
            }
        } else {
            if (typeof arg === "function") {
                name = getFunctionName(arg);
                // Применяем все атрибуты
                // т.е. заменяем расширенным вариантом
                for (var j = 0; j < advices.length; j++) {
                    var advice = advices[j];
                    arg = advice.apply(arg);
                }
                // Добавим функцию в глобальную область видимости
                eval("window." + name + " = arg;");
            }
            // Очищаем списох собранных атрибутов
            advices.length = 0;
        }
    }
}

function getFunctionName(fn) {
    var source = fn.toString();
    var head = source.split("(")[0];
    var fnName = head.split(" ")[1];
    return fnName;
}

Организуем расширение функциональности

function Advice(implementation) {
	this.implementation = implementation;
}

// fn - функция для которой применяем атрибут
Advice.prototype.apply = function(fn) {
	if (typeof fn !== "function") {
		throw "You can apply an advice only to a function";
	}
	// возвращаем расширенную функцию
	return this.implementation(fn);
}

И добавим необходимые нам атрибуты

function OnBeforeAdvice(onBeforeHandler) {
	return new Advice(function(fn) {
		return function() {
			onBeforeHandler(fn, arguments);
			return fn.apply(this, arguments);
		}
	});
}

function OnAfterAdvice(onAfterHandler) {
	return new Advice(function(fn) {
		return function() {
			var result = fn.apply(this, arguments);
			onAfterHandler(fn, arguments, result);
			return result;
		}
	});
}

function OnErrorAdvice(onErrorHandler) {
	return new Advice(function(fn) {
		return function() {
			try {
				return fn.apply(this, arguments);
			} catch (e) {
				onErrorHandler(fn, arguments, e);
			}
		}
	});
}

Все готово, можно создавать методы и расширять их

 <script type="text/javascript">
	AOP(
		[OnBeforeAdvice(onBeforeHandler)],
		[OnAfterAdvice(onAfterHandler)],
		function sqr(x) {
			return x * x;
		},

		[OnAfterAdvice(onBeforeHandler)],
		function A() {
		},

		[OnErrorAdvice(onErrorHandler)],
		function throwsException() {
			throw "Exception";
		}
	);
	
	function onBeforeHandler(fn, args) {
		console.log("I know that somebody calls " + fn.toString().split("(")[0]);
	}

	function onAfterHandler(fn, args, result) {
		console.log("Returns " + result + " from " + args[0]);
	}

	function onErrorHandler(fn, args, e) {
		console.log(e + " was thrown");
	}

	onload = function () {
		sqr(10);
		A();
		throwsException();
	}
</script>

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

В статье на википедии есть ссылки на реализации АОП для JavaScript, можно сравнить. И еще один проект Aspect JS

Автор: pifarik


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js