Аспе?ктно-ориенти?рованное программи?рование (AOP) довольно популярная парадигма программирования. В статье на Wikipedia хорошо объяснена мотивация этого подхода.
AOP является отличным инструментом для глобальных концепций, таких как: логирование, которое, напрямую, не влияют на логику кода.
Однако, проблемы с AOP обнаруживаются, когда он используется для бизнес-требований, таких как авторизация. Такие аспекты должны быть четко видны в соответствующем коде, чтобы разработчик мог сразу увидеть, при чтении исходного кода, правильно ли они реализованы. AOP фреймворки обычно решают эту проблему с помощью аннотаций:
Однако, с точки зрения читабельности, он не сильно отличается от функционального подхода, использования метода requireRole вместо аннотации.
Более того, функциональный подход имеет преимущество масштабирования до более сложных проверок прав доступа, таких как анализ параметров метода, прежде чем решить, какая роль пользователя требуется.
То же самое верно и для других типов аспектов, таких как транзакции. К сожалению, функционально представлять более сложные концепции в Java громоздко и неудобно, что создает искусственную популярность AOP фреймворков в Java экосистеме.
В Kotlin вместо Java-подобного подхода к транзакциям с помощью AOP и аннотаций, вроде этого:
Он так же читабелен и выглядит чисто, когда переписан без применения аннотаций, с функциональным подходом.
Преимущество этого подхода заключается в том, что всегда можете нажать Ctrl / Cmd + Click на объявлении функции transactional в вашей IDE и сразу увидеть, что именно она делает. Что обычно невозможно с любым из обычно используемых AOP фреймворков. Даже когда навигация к коду аспекта обеспечивается с помощью плагина IDE, для расшифровки его логики требуется знание отдельного многофункционального API и/или соглашений.
К сожалению, есть проблемы с масштабированием способа замены AOP аннотаций в Kotlin. Для случая когда несколько аспектов применяются к одной и той же функции, с накоплением фигурных скобок и отступов:
Обходное решение — создать higher-order function, чтобы сохранить приемлемый код при применении нескольких аспектов:
Другим недостатком функционального подхода является то, что такие аспекты, как логирование, требуют доступа к параметрам метода. Они обычно доступны в AOP фреймворках через специальные API, но с использованием стандартных функций языка Kotlin нельзя легко получить к ним доступ. Таким образом, для того, чтобы на самом деле представить реальный пример аспекта логирования, в чисто функциональном виде, все еще нужно написать значительное количество boiler-plate кода:
Это соображение все еще делает AOP предпочтительным инструментом для логирования, когда вам действительно необходимо иметь его глобально и согласованно в вашем приложении. При этом использование AOP для такой функциональности как авторизация и транзакции, является злоупотреблением, учитывая богатые функциональные абстракции, которые доступны в Kotlin. Функции справляются с этими требованиями лучше и чище.
В заключение я бы сказал, что дальнейшее усовершенствование функциональных абстракций для обеспечения еще лучшей замены АОП могло бы стать многообещающим вектором будущего развития языка Kotlin. AOP фреймворки, основанные на Java, обычно специфичны для JVM и воспринимаются как магия, в то время как функциональные абстракции Kotlin действительно кроссплатформенны и прозрачны для пользователя.
AOP является отличным инструментом для глобальных концепций, таких как: логирование, которое, напрямую, не влияют на логику кода.
Однако, проблемы с AOP обнаруживаются, когда он используется для бизнес-требований, таких как авторизация. Такие аспекты должны быть четко видны в соответствующем коде, чтобы разработчик мог сразу увидеть, при чтении исходного кода, правильно ли они реализованы. AOP фреймворки обычно решают эту проблему с помощью аннотаций:
@RequireRole(Role.Admin) // clearly visible aspect
fun updateUserPermissions(…) {
// logic here
}
Однако, с точки зрения читабельности, он не сильно отличается от функционального подхода, использования метода requireRole вместо аннотации.
Примечание переводчика: в оригинальной статье используется выражение when rewritten in a functional way или functional approach что можно трактовать как использование функционального подхода так и прямого/конкретного вызова функции. В статье есть примеры которые можно трактовать по-разному.
Так же:
1. В дальнейшем мы вернемся к понятию Higher-Order Functions
2. В статье встречается слово аспект, что является англицизмом и понятием в AOP aspect
fun updateUserPermissions(…) {
requireRole(Role.Admin) // throws SecurityException
// logic here
}
Более того, функциональный подход имеет преимущество масштабирования до более сложных проверок прав доступа, таких как анализ параметров метода, прежде чем решить, какая роль пользователя требуется.
То же самое верно и для других типов аспектов, таких как транзакции. К сожалению, функционально представлять более сложные концепции в Java громоздко и неудобно, что создает искусственную популярность AOP фреймворков в Java экосистеме.
В Kotlin вместо Java-подобного подхода к транзакциям с помощью AOP и аннотаций, вроде этого:
@Transactional
fun updateUserPermissions(…) {
// logic here
}
Он так же читабелен и выглядит чисто, когда переписан без применения аннотаций, с функциональным подходом.
fun updateUserPermissions(…) = transactional {
// logic here
}
Преимущество этого подхода заключается в том, что всегда можете нажать Ctrl / Cmd + Click на объявлении функции transactional в вашей IDE и сразу увидеть, что именно она делает. Что обычно невозможно с любым из обычно используемых AOP фреймворков. Даже когда навигация к коду аспекта обеспечивается с помощью плагина IDE, для расшифровки его логики требуется знание отдельного многофункционального API и/или соглашений.
К сожалению, есть проблемы с масштабированием способа замены AOP аннотаций в Kotlin. Для случая когда несколько аспектов применяются к одной и той же функции, с накоплением фигурных скобок и отступов:
fun updateUserPermissions(…) = logged {
transactional {
// logic here
}
}
Обходное решение — создать higher-order function, чтобы сохранить приемлемый код при применении нескольких аспектов:
fun updateUserPermissions(…) = loggedTransactional {
// logic here
}
Другим недостатком функционального подхода является то, что такие аспекты, как логирование, требуют доступа к параметрам метода. Они обычно доступны в AOP фреймворках через специальные API, но с использованием стандартных функций языка Kotlin нельзя легко получить к ним доступ. Таким образом, для того, чтобы на самом деле представить реальный пример аспекта логирования, в чисто функциональном виде, все еще нужно написать значительное количество boiler-plate кода:
fun updateUserPermissions(params: Params) =
logged("updateUserPermissions($params)") {
// logic here
}
Это соображение все еще делает AOP предпочтительным инструментом для логирования, когда вам действительно необходимо иметь его глобально и согласованно в вашем приложении. При этом использование AOP для такой функциональности как авторизация и транзакции, является злоупотреблением, учитывая богатые функциональные абстракции, которые доступны в Kotlin. Функции справляются с этими требованиями лучше и чище.
В заключение я бы сказал, что дальнейшее усовершенствование функциональных абстракций для обеспечения еще лучшей замены АОП могло бы стать многообещающим вектором будущего развития языка Kotlin. AOP фреймворки, основанные на Java, обычно специфичны для JVM и воспринимаются как магия, в то время как функциональные абстракции Kotlin действительно кроссплатформенны и прозрачны для пользователя.
Примечание переводчика:
1. Cтатья на medium (eng) .
2. Автор оригинальной статьи является Roman Elizarov (Team Lead JetBrains, working on Kotlin coroutines and libs, sports programming/ICPC, concurrency & algorithms, math/quantitative finance; formerly Devexperts). На Хабре elizarov
Throwable
Основная разница в том, что AOP-фреймворк создает объект динамически, инъектируя поведение в зависимости от текущиего контекста и его конфигурации. А вот в функциональном подходе поведение линкуется статически. Для обеспечения гибкости поведение можно также делегировать контексту, но ссылку на него придется указывать дополнительно. То есть либо придется всегда передавать контекст в качестве одного из параметров, либо создавать глобальный объект-синглтон для контекста, ну или как вариант инъектировать его через ThreadLocal.
sshikov
Именно. То что тут описано — это в некотором смысле вообще не AOP, а точнее — только один из его частных случаев. Претензии же вида «Такие аспекты должны быть четко видны в соответствующем коде» вообще выглядят странно. Мне приходилось довольно активно применять AOP для того, чтобы модифицировать поведение legacy кода, который про эти самые аспекты ни сном ни духом. И это является как раз плюсом, legacy код про аспекты знать не знает — при этом они его поведение успешно улучшают (ну, или как повезет ;).