AOP vs Функции

Аспе́ктно-ориенти́рованное программи́рование (AOP) довольно популярная парадигма программирования. В статье на Wikipedia хорошо объяснена мотивация этого подхода.

image

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
Поделиться публикацией

Похожие публикации

Комментарии 2

    +1

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

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

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое