Когда может пригодиться экзотика в ООП: миксины/трейты/аспекты

от автора

Когда может пригодиться экзотика. Я много раз обращался к использованию множественного наследования в языках программирования и платформах. Но как правило это обращение было больше теоретическим. Я не видел в его применении значительных преимуществ, способных превзойти альтернативные подходы, с учётом рисков его применения. Но в это раз случай представился. И я покажу этот пример. Итак, множественное наследование не ново. Есть сырое жёсткое применение в лоб в плюсах. Когда появилась 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 internallyinterface AspectDirectWrap<In, Out extends IWrapper>extends AspectBase<In,Out>, AspectInOut<In,Out> {// getter@Overridedefault Out in2out(In el) {return wrap(el);}// setter@Overridedefault In out2in(Out el) {return unwrap(el);}}

Аспект варианта “обратное разворачивание”

interface AspectReverseUnwrap<In extends IWrapper, Out>extends AspectBase<Out,In>, AspectInOut<In,Out> {// getterdefault Out in2out(In el) {return unwrap(el);}// setterdefault 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#, Скала. Другие варианты. А относительно плюсов текущий год обещает подвезти нам рефлексию в стандарт, что позволмт всякие биндинги делать гораздо проще.

ссылка на оригинал статьи https://habr.com/ru/articles/1056780/