Предлагаю взглянуть на эту работу:
CZ: Multiple Inheritance Without DiamondsТам разобраны основные популярные техники (но не все) организации интерфейсов как обобщённых алгоритмов, а именно: множественное наследование, "trait" (абстрактные типы, "типажи" как в Rust, классы типов как в Haskell, протоколы, прототипы, интерфейсы), "mixin" ("подмешивания", "примеси"). Указаны ключевые проблемы, и предложен свой вариант реализации множественного наследования с предохранением от классических граблей от "ромбов". Прежде всего -- проблемы с конструкторами данных. Ну и с именами конфликтующих элементов, хотя здесь принципиально решений всяких много, а то даже шире -- как в Scala с "path-dependent types", здесь кратко пример (если концепция незнакома):
http://danielwestheide.com/blog/2013/02 ... types.htmlВ двух словах. Выделяются две семантические языковые роли у типа-класса: как единица (повторного) использования и как единица создания экземпляров-объектов. Абстрактные классы лишь в первой роли, однако могут содержать свои данные (ессно и операции), но не могут вызывать конструкторы данных и "требуют" организацию инициализации у своих потомков. Введено два ключевых слова: require -- понятие лишь субтипа, extends -- в т.ч. и субкласс (т.е. и возможность создания экземпляров). Имеется некий контроль типов (недопущение ромбов-субклассов), определена политика диспетчеризации мультиметодов (особенно см. гл.5 "Example: Abstract Syntax Trees").
В общем, техника максимально близка к "классическому" ("статическому") ООП.
В IT также имеется и "динамическое" ООП. В качестве примера (диалект ML для генерации С++):
objects:
http://felix-lang.org/share/src/web/tut ... index.fdocpolymorphism:
http://felix-lang.org/share/src/web/tut ... index.fdocПервая ссылка -- вариант ОПП в "прототипном" стиле (аля JavaScript, решений для Lua и т.п.). Фактически -- абстракция над записями (record или структуры), позволяющая динамически создавать/удалять "потомков". Только одиночное наследование -- каждый тип подразумевается как роль, объект в целом обладает множеством ролей. Интерфейсы как в Java, или аля как было в COM. Возможна не только "реализация" интерфейса в типе, но и "преобразование" типа для интерфейсной переменной (в общем случае её можно понимать как структуру с указателями на функции). Интерфейс может быть определен (составлен) без оглядки на реальные структуры объектов, их иерархию и т.д. (там даже есть примеры, когда в одной подсистеме (программе, или аля DLL) объекты "живут" по-своему согласно своим типам, а в другую подсистему передаются для взаимодействия уже под нужными интерфейсами согласно местной "точки зрения" в данной системе). Чтобы не было "случайных утиных" типов требуется явное приведение типа (разработчик "номинативно" подтверждает семантику).
По второй ссылке выше -- классы типов аля Haskell (ну и там попутно примеры насчёт полиморфизма с ограничениями типов, включая понятие множества типов). Или же это trait-и без данных согласно статейке выше про проект CZ. Ключевое -- только статическая диспетчеризация типов. Здесь, в отличие от понимания интерфейса, уже семантически подразумевается возможность указания реализации по умолчанию, ограничений (в т.ч. и как "серый или белый ящик", когда в интерфейсах возможны лишь требования как к "чёрному ящику"). Т.е. здесь классы -- единицы использования или реализации (но без своих данных), интерфейсы выше -- некие единицы взаимодействия.
Фактически, в данном случае классы всего лишь обвёртки над статической перегрузкой функций по типам. В целом там все функции перегружаются по типам и арности, в отличие от того же Haskell-я. Но последствие -- затруднителен автоматич. вывод типов -- фактически приходится указывать сигнатуры типов функций явно (что даже хорошо в масштабных проектах, или же без сигнатуры подразумеваются абстрактные типы-"generic"-и), внутри функций локальные автоопределения типов вполне возможны.
Итак, если сопоставить с решением на принципах множественного наследования как в CZ выше, то заметна принципиальная разница -- в целом, через одиночное наследование выстраивается иерархия (когда нужно) "единиц создания экземпляров", а "единицы обобщённого использования" (в общем случае -- с неизбежным множественным наследованием, вне зависимости от того, как оно в языке/платформе понимается или обзывается) -- как-то сбоку, подключаются или инъектируются -- линейная модель, которая, как минимум, возможно проще воспринимается, оценивается и т.п.
Однако конкретно в данном случае в классах типов имеются универсальные недостатки, впрочем как и во многих популярных языках/платформах с trait-ами, не имеющих данных (а те, которые имеют -- абстрактные классы, разобранные в CZ, в той же статье указано и о проблемах trait-ов без данных). На примере того же Rust -- ниже ссылка на предложение ввести в trait-ы виртуальные поля данных:
https://github.com/nikomatsakis/fields- ... -traits.mdКроме конкретных технических нюансов данной платформы (некоторые проблемы с типизацией, "borrow checker") в целом без виртуальных полей возникает потребность явно вводить всякие функции-"accessor"-ы (и соответственно их реализовывать), к тому же семантически необходимо их как-то всё-равно выделять и разруливать политику public/ptotected или private, понимая, что касается именно реализации, а что именно доступа со стороны клиентов будущего типа.
Т.е., растут аппетиты, хочется чего-либо аля "mixin", как во всяких скриптовых языках или вот на примере языка D -- текст примерчика (просто под руку попался) выложили в каментах к статье опять же вокруг Rust-а:
https://habrahabr.ru/post/309968/#comment_9808142Но в D есть некая каша -- "binding" полей косвенный через совпадение имён (может и есть какое-то явное управление), наблюдается прямая инициализация собственного состояния (однако нюансов не знаю, возможно автоматом ничего не исполняется в плане вызова конструкторов). В статейке про CZ хорошо разложено, почему инициализацией должны управлять только лишь "единицы создания экземпляров" -- иначе множественное наследование не разрулить. Отсюда, кстати, не получится соблазн -- иметь "типаж" (trait) как абстрактный тип, но при случае он же может быть и полноценным типом (т.е. trait как полноценный готовый type). Однако реализация (использование) trait-а м.б. тривиальной -- добавить инициализацию.
Поэтому в целом логично, если в trait-ах будут лишь виртуальные структуры. Правда, язык должен быть по-удобнее, в дополнение к стилю (аля как предлагается в Rust):
Код:
trait Trait {
field1: Type1,
field2: Type2;
fn foo();
}
...
impl Trait for Type {
field1: self.foo.bar,
field2: self.baz
}
не помешают и прочие стили параметризации аля ML, напр., как "аргументы":
Код:
trait Trait(field1: Type1, field2: Type2) {
fn foo();
}
...
struct Type {
...
impl Trait(self.foo.bar, self.baz) {
fn foo() => ...
}
}
(trait-ы рекурсивно по своей иерархии передают параметры или инициализируют их явно, типы-кортежи для вирт. полей или параметров, ессно, могут определяться отдельно). Нужны и прочие фишки, как partial-определение типов (подключение функционала по потребности).
Кстати, стоит принципиально отличать подобные "типажи" от "наследования" в виде "embedding" как в Go ("композиция" со включением полей плюс автоматом полученный интерфейс наружу с неким разруливанием имён):
https://github.com/luciotato/golang-not ... ter/OOP.mdВ общем, такая общая картина.