Когда может пригодиться экзотика в ООП: миксины/трейты/аспекты
- четверг, 9 июля 2026 г. в 00:00:10
Когда может пригодиться экзотика.
Я много раз обращался к использованию множественного наследования в языках программирования и платформах. Но как правило это обращение было больше теоретическим. Я не видел в его применении значительных преимуществ, способных превзойти альтернативные подходы, с учётом рисков его применения. Но в это раз случай представился. И я покажу этот пример.
Итак, множественное наследование не ново. Есть сырое жёсткое применение в лоб в плюсах. Когда появилась Scala, я присматривался к её трейтам.
В общем даже ситуации для их использования попадались. Например, удобно было бы в java ORM (JPA) ввести некие свойства, которые бы автоматически управляли обновлением полей в случае двунаправленных зависимостей. Нужно было присоединить их к геттерам/сеттерам. Но как-то не рискнул я тогда ради одного нишевого применения переползать на новую платформу, в скале есть много специфики (например, неустойчивые системы сборки). Обошёлся в итоге лямбдами, хорошо они тогда уже появились в Java.
Ну а самый первый и опасный опыт - это конечно C++. Здесь множественность с типичной для языка свободой, которая сулит перспективу напороться на подводные камни. Из проблем, с которыми лично я помаялся - усложнение привидения указателей. Требуется аккуратное с ними обращение, статический каст сразу можно забыть и собирать программу с включенным rtti, что не всегда доступно. Плюс сюрпризы, связанные конкретно со стандартной библиотекой - использование шаблонов перпендикулярно виртуальным методам и попытки наследоваться, да ещё множественно, от, например, потоков вывода, доставят вам кучу проблем. Благо, есть в плюсах настоящее ООП. Смотрим в сторону Qt. Местами мультиклассы из коробки. Но есть нюансы - qobject только по генеральной линии, примерно как в java.
Где ещё есть mixins / traits / aspects? Говорят про php и Ruby. Я не имел возможности оценить все достоинства. Что-то наподобие присоединённых свойств/методов в Котлине? Ок, но опять же пользоваться Котлином с его постоянно ломающейся обратной совместимостью адски неприятно. Несмотря на синтактический сахар.
Давайте обратно к Java вернёмся. Ранее гуру советовали обходиться методом композиции вместо наследования для использования сторонней логики. Но это не всегда удобно, в частности для интерфейсов с множеством методов и/или сложной иерархией в той же сторонней логике. Я видел, как агрегация выливается в перетаскивание жирных настроечных контекстов по коду программы.
В 8 версии завезли интерфейсные методы с телом, что некоторых сторонников чистого ООП огорчило. Но они позволяют теперь реализовать что-то вроде миксинов и даже трейтов на их основе. В Вебе есть примеры на эту тему.
А мне сейчас миксины пригодились вот для чего: надо было добавить аспекты в иерархию. Причем как полагается логика аспекта перпендикулярна основной, заложенной в классы. Если более точно, то нужно создать зеркало коллекций в виде адаптеров. Адаптеры отличаются тем, что оборачивают объекты бизнес-логики в интерфейсный контракт. И в таком виде отдаются внешним модулям-клиентам. Клиенты ранее пользовались логикой напрямую, и все коллекции были просто коллекциями. Теперь же нужно сделать замену, но так, чтобы этого никто не заметил. Итак, теперь вместо объектов имплементации (с префиксом J ) имеем оболочки с префиксом I (что значит интерфейс). Адаптеры коллекций будут помогать нам скрывать произведённую афёру.
Как они это делают? Они хранят оболочки (IWrapped) реализаций. Клиентам они показывают интерфейсную часть, то есть оболочки. А вот наш модуль видит всю их подноготную (реализацию), причём только её. Он не интересуется, как там выглядит это всё на большом экране. Тут за кулисами и декорациями происходит основная работа. Гениально!
Сразу отмечу, что помимо шаблона Адаптер, рассматривалось применение Прокси. Помимо выигрыша в скорости и переносимости, что спорно, адаптер упускает ряд деталей, некоторые из которых могут быть важны. Как то: сломанный instanceof по умолчанию и сравнение по ссылке а также другие аспекты рефлексии.
Всё бы хорошо, но я как-то забыл, что коллекции могут создаваться в зрительном зале. Оттуда вам шлют сообщения целыми пачками (коллекциями). Типа “браво!”, “бис”, “халтура”, “судью на мыло”, “заменить все красные карточки на прозрачные”. Иногда в одном предложении. Это завернутые (зашифрованные) сообщения. Их надо обработать - обернуть в коллекцию, которая будет их РАЗворачивать.
Теперь имеем два места действия: закулисье - здесь сообщения для работы разворачиваются на выходе из коллекции и заворачиваются на входе - добавлении в коллекцию, и партер - здесь наоборот, разворачивание при вставке, заворачивание при запросе.
При этом коллекции - адаптеры. Есть внутреннее ядро и внешний интерфейс. Есть две области: внешняя и внутренняя. Это специфика коллекций. Коллекции в стандартной иерархии - Iterator, Iterable, Collection, List… Все с ядром и фэйсом.
И вот тут возникает сложность. Кроме этих двух областей, есть две сферы применения - зрительный зал и закулисье. И что делать? Писать две версии всей иерархии, которой и так ~5-6 классов. Дублирование логики…

И вот тут возникает идея вынести аспекты логики… в Аспекты! Ну или миксины, кому как больше нравится.
Итак. Имеем пространство из двух координатных осей.
Физическое разложение самих коллекций на область внешнюю (адаптер/прокси) и внутренне ядро. In -> Out. Скажем, это координата вертикали (или горизонтали).
Далее есть два пространства пользования: зал и сцена. То есть имеются декорированные интерфейсы-оболочки (IWrapper) и реализации в сыром виде, причём не только коллекций. Impl -> WrT. Назовём это координатой вращательного движения, то есть угла поворота: Правовращательные и левовращательные. Хотя тут скорее завёртывание -> развёртывание.
Как видим, на самом деле координаты независимы. И использовать их следует именно так. Такой подход позволил мне реализовать логику всего за час. Более того, после успешной компиляции оно заработало без ошибок сразу, с ходу. Без единой ошибки! Сколько бы мне пришлось помучиться, если бы я писал это на ифах.
В плане реализации конечно было бы правильнее иметь миксины без обязательного вынесения их методов в контракт. Но пока что имеем.
Теперь собственно код. Основные интерфейсы:
/** * @param <Store> IN-ternal type of elements stored in delegate * @param <Exp> Out, export or interface type, visible outside */ interface AIterator<Exp,Store> extends Iterator<Exp>, AspectInOut<Store,Exp> {} interface AIterable<Exp,Store> extends Iterable<Exp>, AspectInOut<Store,Exp> {} interface ACollection<Exp,Store> extends AIterable<Exp,Store>, Collection<Exp> {} interface AList<Exp,Store> extends ACollection<Exp,Store>, List<Exp> {}
База аспектов
interface AspectBase<Impl,WrT extends IWrapper> { Impl unwrap(WrT wr); WrT wrap(Impl wr); }
Аспект ввода-вывода
interface AspectInOut<In,Out> { Out in2out(In el); In out2in(Out el); }
Аспект варианта “прямое разворачивание”
// collection created by Impl internally interface AspectDirectWrap<In, Out extends IWrapper> extends AspectBase<In,Out>, AspectInOut<In,Out> { // getter @Override default Out in2out(In el) { return wrap(el); } // setter @Override default In out2in(Out el) { return unwrap(el); } }
Аспект варианта “обратное разворачивание”
interface AspectReverseUnwrap<In extends IWrapper, Out> extends AspectBase<Out,In>, AspectInOut<In,Out> { // getter default Out in2out(In el) { return unwrap(el); } // setter default In out2in(Out el) { return wrap(el); } }
База
abstract class AdapterBase<Impl extends PlatformBase, WrT extends IWrapper> implements AspectBase<Impl, WrT> { protected Function<Impl, WrT> adaptor; protected Class<Impl> implType; AdapterBase() { checkSetAdapter(null); } public final WrT wrap(Impl iEl) { checkSetAdapter(iEl); return adaptor.apply(iEl); } public final Impl unwrap(WrT wr) { return wr.unwrap(); } private boolean checkSetElType(Impl el) { if (implType == null) { if (el == null) return false; implType = (Class<Impl>) el.getClass(); var base = AbstractWrapper.searchBaseTypeOfDelegate(el); if (base != null) implType = (Class<Impl>) base; } return true; } private void checkSetAdapter(Impl el) { if (adaptor == null && checkSetElType(el)) { adaptor = getInstanceWrapperFactory(implType); } } }
Сущности
/** * * @param <Impl> type of implementation element * @param <WrT> type of wrapped element * @param <Store> IN-ternal type of elements stored in delegate * @param <Exp> Out, export or interface type, visible outside */ abstract class IteratorAdapterInt<Impl extends PlatformBase, WrT extends IWrapper,Store,Exp> extends AdapterBase<Impl,WrT> implements AIterator<Exp,Store> { protected final Iterator<Store> adapted; IteratorAdapterInt(Iterator<Store> adapted) { this.adapted = adapted; } @Override public boolean hasNext() { return adapted.hasNext(); } @Override public Exp next() { return in2out(adapted.next()); } @Override public void remove() { adapted.remove(); } } class IteratorAdapter<Impl extends PlatformBase, WrT extends IWrapper> extends IteratorAdapterInt<Impl,WrT,Impl,WrT> implements AspectDirectWrap<Impl,WrT> { IteratorAdapter(Iterator<Impl> adapted) { super(adapted); } } class IteratorAdapterRev<Impl extends PlatformBase, WrT extends IWrapper> extends IteratorAdapterInt<Impl,WrT,WrT,Impl> implements AspectReverseUnwrap<WrT,Impl> { IteratorAdapterRev(Iterator<WrT> adapted) { super(adapted); } } // ==== Iterable adapter ========================== abstract class IterableAdapterInt<Impl extends PlatformBase, WrT extends IWrapper,Store,Exp> extends AdapterBase<Impl,WrT> implements IWrapper, AIterable<Exp,Store> { protected final Iterable<Store> adapted; IterableAdapterInt(Iterable<Store> adapted) { this.adapted = adapted; } @Override public Iterator<Exp> iterator() { return new Iter(adapted.iterator()); } class Iter extends IteratorAdapterInt<Impl,WrT,Store,Exp> { Iter(Iterator<Store> adapted) { super(adapted); } @Override public Exp in2out(Store el) { return IterableAdapterInt.this.in2out(el); } @Override public Store out2in(Exp el) { return IterableAdapterInt.this.out2in(el); } } public Iterable<Store> unwrap() { return adapted; } } class IterableAdapter<Impl extends PlatformBase, WrT extends IWrapper> extends IterableAdapterInt<Impl,WrT,Impl,WrT> implements AspectDirectWrap<Impl,WrT> { IterableAdapter(Iterable<Impl> adapted) { super(adapted); } }
Как видите, основные базы иерархии включают по 4 параметра - по 2 на каждую ось. <Impl, WrT, Store, Exp>
Отмечу, что за кулисами происходит подмена языковой модели на другую - от явы к плюсам. В зале сидят классы модулей генерации jaxb. Для тех, кто не знает, это биндинг сущностей языка на xml. Что-то похожее на ORM, только для xml (возможен json, yaml). Подмена языка предполагает, что им может быть и другой похожий - TS, Js, C#, Скала. Другие варианты. А относительно плюсов текущий год обещает подвезти нам рефлексию в стандарт, что позволмт всякие биндинги делать гораздо проще.