В моей первой статье на хабре речь пойдет о комбинации примитивных конструкций, позволяющих организовать наследование реализаций и композицию состояний. Поочередно разберу, от простых в использовании конструкций, до комплексных prod-ready решений, которые могут найти повсеместного применения в разработке и публичных контейнерах. Здесь не будет зависимостей, макросов, Rc, Box и тд. - исключительноno_std. Мнение относительно ООП разнится, кто-то против него, кто-то (как в UI) без него ставят крест на ржавом. В Rust Book, в 17 главе приводят: "Если язык должен иметь наследование, чтобы быть объектно-ориентированным, то Rust таким не является. Здесь нет способа определить структуру, наследующую поля и реализации методов родительской структуры, без использования макроса". Сегодня я покажу, что такое возможно и считаю, что такой подход можно использовать разумно и безопасно, что не помешало бы большому количеству контейнеров, где для собственных реализаций Middleware и Response приходится писать либо тонну однообразного кода, что совсем не DRY; либо разбивать логику на кучу компонентов. Не буду томить, текста и так много - основой наследования станет незамысловатая конструкция из четырех строчек, которую и начну развивать дальше: Объявление трейта с зависимостью от собственного типа Сигнатура функции, избитой во всех примерах ООП (для простоты понимания) Благодаря возможности стандартной реализации, можно рекурсивно вызывать функцию из родительского типа И уже более замысловатый пример с объявлением наследственной иерархии животного: Создаем статическую структуру Dog и Cat, без заранее известного размера присвоить тип невозможно. В обоих случаях используется type Parent = Self, чтобы явно указать, что родителей не имеется и вся логика при распутывании иерархии закончится на них. Важно при этом покрыть все методы, чтобы не создать бесконечный цикл из-за стандартной реализации. Для разнообразия возможностей полиморфизма с такой конструкцией, добавим две функции с почти идентичной сигнатурой: A: Animal - принимает все вариации с имплементированным базовым трейтом, A: Animal<Parent = Cat> - принимает реализации с конкретным родителем. При использовании этой конструкции (да и во всех последующих) появляется пару неудобных моментов, держите это в уме, если решите использовать этот подход: Нам приходится дублировать сигнатуру вызова функции, добавляя Self::Parent к каждому вызову в стандартной реализации, с другой стороны появляется больше контроля за промежуточными вызовами, которые можно обвесить логами/трасерами/таймером или передаче параметров не включенных в аргументы, например, из глобального мьютекса. При добавлении нового метода, появляется опасность вызвать непокрытую зацикленную функцию и получить панику с Stack overflow. Как вариант исправления, использовать базовую структуру с имплементированными паниками или стандартным поведением на вершине иерархии, так сказать замыкаясь, что и будет использоваться дальше. Конструкция описанная выше годится, например, для описания логики алгоритма, позволяя переносить отдельные функции из версии в версию, не переписывая код и не разбивая логику на несколько трейтов. Но этого мало, добавляем наследование реализации с учетом состояния: Теперь для обращения к родителю используется функция parent и рекурсия в методах использует ее для вызовов. По рекомендации из прошлого примера, добавляем стандартную реализацию в виде Animal, хранящего префикс и метод для озвучивания. parent ссылает на себя, потому что иерархия вызовов должна замкнуться на нем. Объявляем Cat c компонентом Animal и билдером префикса для всех наследников (для лаконичности так будут объявляться компоненты и дальше). При имплементации Animal необходимо только указать на состояние компонента, метод озвучивания подтянется автоматически. Объявляем Winky с компонентом Cat. Как и в пункте выше, указываем только на состояние, причем из-за того что Cat уже наследует Animal, углубляться в состоянии не требуется. Но при необходимости можно и перегрузить метод, как в случае с Komaru, в этом случае можно даже использовать type Parent = Self, так как состояние не нужно. Как и в примере выше, возможно разграничить доступ к функциям на основе Parent, если бы мы убрали Cat у Komaru, то компилятор бы ее не пропустил! Перечисленных сверху конструкций уже достаточно для безопасной работы наследования в ржавом, но есть возможность наследования реализации через баунды, что при должном использовании может быть неплохим вариантом: Почти идентичный первому примеру вариант, с одним отличием, вместо type Parent используется баунд, что позволяет ему унаследовать несколько реализаций и быть в зависимости от ситуацииCat и Dog. Увы, глубина наследования при множественном наследовании одинаковых типов ограничена всего одним наследником. Рассмотрим использование баундов уже для наследования состояния: Почти также минимум отличий, кроме использования баундов вместе type Parent. Важно то, что состояния не конфликтуют друг с другом и используются в зависимости от ситуации или явного каста. При должном использовании, четыре варианта наследования могут дополнять друг друга, конструкции открыты и не мешают развивать их в необходимом направлении. Всего две строчки могли бы изменить ход развития экосистемы, появившись оно немного раньше и, возможно, эта статья поможет начать внедрять недоступные раньше или изобретать новые паттерны проектирования. С другой стороны забавно осознавать, что всего две строчки оставались недоступны сообществу на протяжении всего развития ржавого и неизвестно еще что из этого получится в будущем, ведь перечисленные механизмы можно перенести на макросы, а описанные абстракции бесплатны в использовании. На написание статьи ушло полгода размышлений и один день жизни, и я надеюсь оно облегчит мне работу с публичными контейнерами в будущем, а код из статьи будет внесен в Rust Book с выдачей мне вечного членства в Rust Foundation.