Реализация миксинов с использованием AOP (AspectJ) или модификации исходного кода (JaMoPP)
В объектно-ориентированных языках программирования миксин относится к определенному количеству функциональных возможностей, которые могут быть добавлены в класс. Важным аспектом этого является то, что он позволяет больше концентрироваться на свойствах конкретного поведения, чем на структурах наследования в процессе разработки.
Например, в Scala вариант mixins можно найти под названием «черты». Хотя Java не обеспечивает прямой поддержки миксинов, их можно легко добавить с помощью нескольких аннотаций, интерфейсов и некоторой поддержки инструментов.
Иногда вы читаете в нескольких онлайн-статьях, что миксины включены в Java версии 8. К сожалению, это не так. Особенностью проекта Lambda ( JSR-335 ) являются так называемые «Виртуальные методы расширения» (VEM).
Хотя они похожи на миксины, они имеют различный фон и значительно более ограничены в функциональности. Мотивация для внедрения VEMs — это проблема обратной совместимости при внедрении новых методов в интерфейсах .
Поскольку в ближайшем будущем «настоящие» миксины не ожидаются на языке Java, в этой статье показано, как уже возможно создать поддержку миксинов в проектах Java, используя простые методы. Для этого мы обсудим два подхода: использование AOP с AspectJ и изменение исходного кода с помощью JaMoPP .
Почему не просто наследство?
На вопрос « Что бы вы изменили в Java, если бы могли ее изобрести заново?» » Джеймс Гослинг , изобретатель Java , как говорят, ответил : « Я хотел бы избавиться от классов «.
После того, как смех утих, он объяснил, что он имел в виду: наследование в Java, которое выражается отношением «расширяет», должно — где это возможно — заменяться интерфейсами [ Почему расширяется — это зло ].
Любой опытный разработчик знает, что он имел в виду: наследование следует использовать с осторожностью. Очень легко использовать его как техническую конструкцию для повторного использования кода, а не моделировать технически мотивированные отношения родитель-потомок с ним.
Но даже если кто-то считает такое технически мотивированное повторное использование кода законным, он быстро достигает своих пределов, поскольку Java не допускает множественного наследования.
Миксины всегда полезны, если несколько классов имеют сходные свойства или определяют сходное поведение, но их нельзя разумно смоделировать просто с помощью тонких иерархий отношений.
В английском языке термины, оканчивающиеся на «способный» (например, «сортируемый», «сопоставимый» или «комментируемый»), часто являются индикатором для применения миксинов. Кроме того, когда вы начинаете писать «служебные» методы, чтобы избежать дублирования кода при реализации интерфейсов, это может указывать на значимый случай применения.
Mixins с АОП
Так называемые объявления между типами являются чрезвычайно простой возможностью для реализации миксинов, предлагаемых проектом AspectJ Eclipse. С их помощью можно, среди прочего, добавлять новые переменные экземпляра и методы в любой целевой класс.
Это будет показано ниже на основе небольшого примера в листинге 1. Для этого мы будем использовать следующие термины:
- Базовый интерфейс Описывает желаемое поведение. Классы, которые миксин не должен использовать, могут использовать этот интерфейс.
- Mixin-Interface Промежуточный интерфейс, используемый в аспекте и реализованный классами, которые должен использовать миксин.
- Аспект Mixin-Provider, который обеспечивает реализацию для mixin.
- Mixin-User Class, который использует (реализует) один или несколько смешанных интерфейсов.
// === Listing 1 === /** Base-Interface */ public interface Named { public String getName(); } /** Mixin-Interface */ public interface NamedMixin extends Named { } /** Mixin-Provider */ public aspect NamedAspect { private String NamedMixin.name; public final void NamedMixin.setName(String name) { this.name = name; } public final String NamedMixin.getName() { return name; } } /** Mixin-User */ public class MyClass implements NamedMixin { // Could have more methods or use different mixins }
В листинге 1 приведен полный пример миксина на основе AOP. Если AspectJ настроен правильно, следующий исходный текст должен скомпилироваться и работать без ошибок:
MyClass myObj = new MyClass(); myObj.setName("Abc"); System.out.println(myObj.getName());
С вариантами АОП можно работать довольно удобно, но есть и несколько недостатков, которые будут рассмотрены здесь.
Прежде всего, объявления между типами не могут иметь дело с универсальными типами в целевом классе. Во многих случаях это не является абсолютно необходимым, но может быть очень практичным. Например, можно также определить интерфейс «Именованный» с помощью универсального типа вместо «Строка». Затем будет определено поведение для любых типов имен. Затем используемый класс может определить, как должен выглядеть тип имени.
Еще одним недостатком является то, что методы, сгенерированные AspectJ, следуют своим собственным соглашениям об именах. Это затрудняет поиск классов с помощью отражения, так как вам придется считаться с именами методов, такими как «ajc $ interMethodDispatch…»
И последнее, но не менее важное: без поддержки среды разработки вы не можете видеть исходный код в целевом классе и зависят только от объявления интерфейса. Это, однако, может рассматриваться как преимущество, поскольку используемые классы содержат меньше кода.
Внешний вид: анализатор и принтер Java Model (JaMoPP)
Альтернативой реализации миксинов с AspektJ является Java Parser and Printer Model (JaMoPP). Проще говоря, JaMoPP может читать исходный код Java, представлять его как граф объектов в памяти и преобразовывать (т.е. записывать) обратно в текст.
Таким образом, с помощью JaMoPP можно программно обрабатывать код Java и, таким образом, автоматизировать рефакторинг или реализовывать, например, собственный анализ кода. Технологически JaMoPP основан на Eclipse Modeling Framework (EMF) и EMFText . JaMoPP совместно разработан Техническим университетом Дрездена и DevBoost GmbH и свободно доступен на GitHub в качестве проекта с открытым исходным кодом.
Mixins с JaMoPP
Далее мы хотели бы взять пример из миксинов АОП и немного его расширить. Для этого мы сначала определим несколько аннотаций:
- @MixinIntf Указывает смешанный интерфейс.
- @MixinProvider Указывает класс, который обеспечивает реализацию для миксина. Реализованный миксин-интерфейс указан как единственный параметр.
- @MixinGenerated Отмечает методы и переменные экземпляра, которые были сгенерированы миксином. Единственным параметром является класс mixin
- поставщик.
В дальнейшем мы также будем расширять интерфейсы и классы из листинга 1 общим типом для имени. Только класс, использующий mixin, определяет, какой конкретный тип должен иметь имя.
// === LISTING 2 === /** Base-Interface (Extended with generic parameter) */ public interface Named<T> { public T getName(); } /** Mixin-Interface */ @MixinIntf public interface NamedMixin<T> extends Named<T> { } /** Mixin-Provider */ @MixinProvider(NamedMixin.class) public final class NamedMixinProvider<T> implements Named<T> { @MixinGenerated(NamedMixinProvider.class) private T name; @MixinGenerated(NamedMixinProvider.class) public void setName(T name) { this.name = name; } @Override @MixinGenerated(NamedMixinProvider.class) public T getName() { return name; } } /** Special name type (Alternative to String) */ public final class MyName { private final String name; public MyName(String name) { super(); if (name == null) { throw new IllegalArgumentException("name == null"); } if (name.trim().length() == 0) { throw new IllegalArgumentException("name is empty"); } this.name = name; } @Override public String toString() { return name; } }
В классе, который должен использовать mixin, интерфейс mixin теперь реализован снова, как показано в листинге 3. Чтобы «смешать» поля и методы, определенные поставщиком mixin, с классом MyClass, используется генератор кода.
С помощью JaMoPP это модифицирует класс MyClass и добавляет переменные экземпляра и методы, предоставляемые поставщиком mixin.
// === LISTING 3 === /** Mixin-User */ public class MyClass implements NamedMixin<MyName> { // Could have more methods or use different mixins }
При этом генератор кода делает следующее. Он читает исходный код каждого класса, подобно обычному компилятору Java, и при этом проверяет количество реализованных интерфейсов.
Если присутствует интерфейс mixin, то есть интерфейс с аннотацией @MixinIntf, соответствующий поставщик найден, а переменные и методы экземпляра копируются в класс, который реализует mixin.
Для того чтобы инициировать генерацию смешанных кодов, в настоящее время есть два варианта: использовать подключаемый модуль Eclipse непосредственно при сохранении или в качестве подключаемого модуля Maven в качестве части сборки.
Инструкции по установке и исходный код обоих плагинов можно найти на GitHub в небольшом проекте SrcMixins4J . Также на экране доступно видео, демонстрирующее использование плагина Eclipse. В листинге 4 показано, как выглядит модифицированный целевой класс.
// === LISTING 4 === /** Mixin-User */ public class MyClass implements NamedMixin<MyName> { @MixinGenerated(NamedMixinProvider.class) private MyName name; @MixinGenerated(NamedMixinProvider.class) public void setName(MyName name) { this.name = name; } @Override @MixinGenerated(NamedMixinProvider.class) public MyName getName() { return name; } }
Если интерфейс mixin удален из раздела «Implements», все поля и методы провайдера, помеченные как «@MixinGenerated», будут удалены автоматически. Сгенерированный код можно переопределить в любое время, удалив аннотацию «@MixinGenerated».
Нажмите на следующее изображение, чтобы открыть Flash-видео, демонстрирующее плагин Eclipse:
Вывод
Поскольку нативная поддержка миксинов в стандарте языка Java не ожидается в обозримом будущем, в настоящее время можно обойтись только с помощью некоторого AOP или генерации исходного кода. Какой из двух вариантов вы выберете, зависит, в основном, от того, предпочитаете ли вы хранить код миксина отдельно от кода вашего приложения или хотите ли вы, чтобы они были непосредственно в соответствующих классах.
В любом случае скорость разработки значительно возрастает, и вы будете меньше концентрироваться на иерархиях наследования и больше на определении функционального поведения.
Ни один подход не идеален. В частности, конфликты не разрешаются автоматически. Методы с одной и той же сигнатурой от разных интерфейсов, которые предоставляются разными провайдерами миксина, приведут, например, к ошибке в классе, который использует оба миксина.
Те, кто ищет что-то еще, должны будут перейти на другой язык с поддержкой родного миксина, например, Scala.