Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

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

Дата публикации: 08-07-2026 02:59:26

Когда может пригодиться экзотика. Я много раз обращался к использованию множественного наследования в языках программирования и платформах. Но как правило это обращение было больше теоретическим. Я не видел в его применении значительных преимуществ, способных превзойти альтернативные подходы, с учётом рисков его применения. Но в это раз случай представился. И я покажу этот пример. Читать далее

Основное содержимое страницы с новостью.

Когда может пригодиться экзотика.

Я много раз обращался к использованию множественного наследования в языках программирования и платформах. Но как правило это обращение было больше теоретическим. Я не видел в его применении значительных преимуществ, способных превзойти альтернативные подходы, с учётом рисков его применения. Но в это раз случай представился. И я покажу этот пример. Итак, множественное наследование не ново. Есть сырое жёсткое применение в лоб в плюсах. Когда появилась 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#, Скала. Другие варианты. А относительно плюсов текущий год обещает подвезти нам рефлексию в стандарт, что позволмт всякие биндинги делать гораздо проще.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1std::expected в C++23: гайд по миграции с исключений на функциональный error handling0707-07-2026
2Ты не найдёшь эту ошибку. Потому что её нет в твоём коде. Как Self-describing API спасает от чужих рефакторингов5807-07-2026
3Интеграция платёжных систем в high-risk вертикалях: архитектура, риски и практический опыт0707-07-2026
4In-memory база врёт: 5 расхождений с продовой БД0707-07-2026
5Технический долг — это не только legacy: как мы уменьшаем разброс решений между Go-сервисами0707-07-2026
6Нужен ли здесь useEffect? «хороший» React-вариант и мол-вариант здесь один и тот же код0507-07-2026
7Plumix: как я переписываю Flutter на C# — вместе с ИИ5707-07-2026
8Шаблоны C++ как инструмент архитектуры: compile-time dispatch, type traits и type erasure5728-06-2026
9Угрожает ли опенсорсу волна сгенерированных пулл-реквестов?0625-06-2026
10Пожалуйста, используйте эти CSS медиа-фукции5707-07-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 5. Источник: habr.com.