Когда может пригодиться экзотика. Я много раз обращался к использованию множественного наследования в языках программирования и платформах. Но как правило это обращение было больше теоретическим. Я не видел в его применении значительных преимуществ, способных превзойти альтернативные подходы, с учётом рисков его применения. Но в это раз случай представился. И я покажу этот пример. Читать далее
Когда может пригодиться экзотика.
Я много раз обращался к использованию множественного наследования в языках программирования и платформах. Но как правило это обращение было больше теоретическим. Я не видел в его применении значительных преимуществ, способных превзойти альтернативные подходы, с учётом рисков его применения. Но в это раз случай представился. И я покажу этот пример. Итак, множественное наследование не ново. Есть сырое жёсткое применение в лоб в плюсах. Когда появилась 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#, Скала. Другие варианты. А относительно плюсов текущий год обещает подвезти нам рефлексию в стандарт, что позволмт всякие биндинги делать гораздо проще.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | std::expected в C++23: гайд по миграции с исключений на функциональный error handling | 0 | 7 | 07-07-2026 |
| 2 | Ты не найдёшь эту ошибку. Потому что её нет в твоём коде. Как Self-describing API спасает от чужих рефакторингов | 5 | 8 | 07-07-2026 |
| 3 | Интеграция платёжных систем в high-risk вертикалях: архитектура, риски и практический опыт | 0 | 7 | 07-07-2026 |
| 4 | In-memory база врёт: 5 расхождений с продовой БД | 0 | 7 | 07-07-2026 |
| 5 | Технический долг — это не только legacy: как мы уменьшаем разброс решений между Go-сервисами | 0 | 7 | 07-07-2026 |
| 6 | Нужен ли здесь useEffect? «хороший» React-вариант и мол-вариант здесь один и тот же код | 0 | 5 | 07-07-2026 |
| 7 | Plumix: как я переписываю Flutter на C# — вместе с ИИ | 5 | 7 | 07-07-2026 |
| 8 | Шаблоны C++ как инструмент архитектуры: compile-time dispatch, type traits и type erasure | 5 | 7 | 28-06-2026 |
| 9 | Угрожает ли опенсорсу волна сгенерированных пулл-реквестов? | 0 | 6 | 25-06-2026 |
| 10 | Пожалуйста, используйте эти CSS медиа-фукции | 5 | 7 | 07-07-2026 |