Перейти к основному содержимому

Объектно-ориентированные концепции в Rust

Разработчику Архитектору
Сначала — общие понятия (раздел 4 "Код")

Если ООП для вас новое, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделезачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.

Ниже — struct, трейты и композиция в Rust (без классического наследования).

Теория и идиомы Rust

Классического наследования классов в Rust нет; идеи ООП выражаются через композицию и трейты (аналог интерфейсов с реализацией по умолчанию).

Понятие ООПКак выражено в Rust
АДТstruct, enum; поведение в impl
Инкапсуляциямодули pub / приватные поля; инварианты в конструкторах
"Наследование"встраивание полей, делегирование; трейт-объекты dyn Trait
Полиморфизм подтиповтрейты, dyn, статическая диспетчеризация
Параметрический полиморфизмgenerics + ограничения трейтов (T: Display)
Безопасностьвладение и заимствование вместо GC

Определения — раздел 4-08-oop. Синтаксис Rust — о разделе Rust.

Кратко для новичка:

  • struct — тип с полями (данные); impl — методы для этого типа.
  • trait — контракт поведения (как interface в Java); тип реализует трейт через impl Trait for Type.
  • Композиция — один struct содержит другие как поля; общий код выносят в трейты с методами по умолчанию.
  • Владение — у каждого значения один владелец; ссылки &T / &mut T заимствуют без копирования.

Объектно-ориентированные концепции в Rust

Материал связывает идеи ООП с идиомами Rust. Держите рядом важные трейты и типы, типы и владение и управляющие конструкции.

Объектно-ориентированное программирование (ООП) представляет собой подход к организации кода, при котором данные и поведение объединяются в структуры, называемые объектами. Эти объекты моделируют сущности реального мира или абстрактные концепции, инкапсулируя состояние и предоставляя интерфейсы для взаимодействия с этим состоянием. В традиционных ООП-языках, таких как Java, C++ или C#, ключевыми элементами являются классы, наследование, полиморфизм и инкапсуляция.

Rust не является классическим объектно-ориентированным языком. Он не предоставляет встроенной поддержки классов или иерархического наследования. Однако Rust позволяет реализовать многие принципы, лежащие в основе ООП, через собственные механизмы языка: структуры, трейты и реализации. Это даёт разработчику гибкость в выборе архитектурного стиля и одновременно обеспечивает безопасность памяти и отсутствие накладных расходов во время выполнения.

Интерактивная схема — класс и объект (псевдокод; в Rust это struct + impl/трейты). Полный разбор принципов: ООП в разделе "Код и разработка".

ТИП Кот
поля: имя, возраст
метод мяукнуть()
КОНЕЦ

объект barsik := Кот { имя: "Барсик", возраст: 3 }
barsik.мяукнуть()

Разбор:

  • Это псевдокод ООП-модели: тип объединяет данные (имя, возраст) и поведение (мяукнуть).
  • объект barsik := ... показывает создание экземпляра с конкретным состоянием.
  • Вызов barsik.мяукнуть() демонстрирует отправку сообщения объекту (в Rust аналог — метод через impl).
  • Блок помогает связать привычную ООП-терминологию с последующими Rust-примерами.

Play ITЗагрузка интерактивного демо…

Минимальный Rust-аналог псевдокода выше:

Код ITЗагрузка примера кода…

Разбор:

  • struct Cat хранит состояние объекта (поля), impl Cat — его поведение (методы).
  • meow(&self) принимает неизменяемую ссылку на экземпляр, не забирая владение.
  • String::from("Барсик") создает владеющую строку для поля name.
  • Вызов barsik.meow() — аналог barsik.мяукнуть() из псевдокода.

Создание значений — литерал, new, Default, билдер

В Rust нет constructor в смысле C++. Есть associated functions в impl (часто new) и структурные литералы:

struct Point {
x: f64,
y: f64,
}

impl Point {
fn new(x: f64, y: f64) -> Self {
Self { x, y }
}

fn origin() -> Self {
Self { x: 0.0, y: 0.0 }
}
}

let p1 = Point { x: 1.0, y: 2.0 };
let p2 = Point::new(3.0, 4.0);

Разбор:

  • Point { x, y } — создание на стеке (или владение передаётся дальше).
  • Self в impl Point — псевдоним Point.
  • Имена фабрик (origin) — соглашение, не ключевое слово.

С трейтом Default:

#[derive(Default)]
struct Config {
timeout_ms: u32,
retries: u8,
}

let cfg = Config::default();

Разбор:

  • derive(Default) генерирует нули/пустые значения для полей.
  • Для инвариантов предпочтительнее явный fn new(...) -> Result<Self, Error>.

Запись в переменные — владение и заимствование

let a = String::from("hello");
let b = a; // владение перемещено; a больше нельзя использовать
// let c = a; // ошибка компиляции

let mut acc = BankAccount::new(100.0);
let r = &acc; // неизменяемое заимствование
// acc.deposit(1.0); // ошибка: acc уже заимствован

Разбор:

  • Присваивание перемещает владение для типов вроде String, Vec, пользовательских struct без Copy.
  • &T и &mut T — заимствование для вызова методов без передачи владения.
  • ООП-объект в Rust — это данные + методы, но правила памяти проверяются компилятором.

Видимость и инкапсуляция — pub, модули, pub(crate)

УровеньСмысл
без pubвидно только внутри текущего модуля
pubвидно снаружи crate (если модуль экспортирован)
pub(crate)видно во всём crate
pub(super)видно родительскому модулю
mod account {
pub struct BankAccount {
balance: f64, // приватное поле
}

impl BankAccount {
pub fn new(initial: f64) -> Self {
assert!(initial >= 0.0);
Self { balance: initial }
}

pub fn balance(&self) -> f64 {
self.balance
}
}
}

Разбор:

  • Поля struct по умолчанию приватны даже у pub struct — снаружи модуля нельзя написать account.balance.
  • Инвариант баланс ≥ 0 задаётся в new и в методах изменения.
  • Это сильнее, чем private в Java: нет reflection для обхода в safe Rust.

&self, &mut self, self в методах

СигнатураКто владеетКогда
fn read(&self)заимствованиегеттеры, draw
fn write(&mut self)изменяемое заимствованиеdeposit, push
fn into_inner(self)забирает владениепотребление объекта
impl Unit {
fn damage(&self) -> i32 { /* читает поля */ 0 }
fn attack(&mut self, target: &mut Unit) { /* меняет target */ }
}

Разбор:

  • Методы — синтаксический сахар для функций с первым параметром self.
  • Два &mut на разные объекты разрешены; два &mut на один объект — ошибка borrow checker.

Абстракция без классов — трейты и dyn

pub trait Repository {
fn find(&self, id: &str) -> Option<String>;
}

pub struct MemoryRepo {
data: std::collections::HashMap<String, String>,
}

impl Repository for MemoryRepo {
fn find(&self, id: &str) -> Option<String> {
self.data.get(id).cloned()
}
}

fn load(repo: &dyn Repository, id: &str) -> Option<String> {
repo.find(id)
}

Разбор:

  • trait Repositoryконтракт (аналог interface).
  • dyn Repository — trait object, динамическая диспетчеризация (vtable).
  • Альтернатива без runtime-cost: fn load<R: Repository>(repo: &R, id: &str) — мономорфизация.

Наследование через композицию

struct Engine;
impl Engine {
fn start(&self) { println!("vroom"); }
}

struct Car {
engine: Engine,
}

impl Car {
fn start(&self) {
self.engine.start();
}
}

Разбор:

  • Вместо class Car extends Vehicle — вложенные поля и явное делегирование.
  • Общий код выносят в функции, макросы или default methods в трейтах:
trait Greeter {
fn name(&self) -> &str;
fn greet(&self) -> String {
format!("Hello, {}", self.name())
}
}

Сравнение Rust с классическим ООП

Идея ООПJava/C#Rust
Классclassstruct + impl
Наследованиеextendsкомпозиция + трейты
Интерфейсinterfacetrait
Полиморфизм runtimevirtualdyn Trait
Полиморфизм compile-timegenericsimpl Trait, <T: Trait>
Инкапсуляциямодификаторымодули + приватные поля
Конструкторnew Type()Type::new() / литерал

Типичные ошибки при переносе мышления из Java

ОшибкаПочему плохоИдиома Rust
Глубокая иерархия structнет subtyping для structтрейты + композиция
Публичные поля для DTOломают инвариантыpub только когда нужно, иначе методы
Rc<RefCell<T>> вездеимитация GCспроектировать владение
dyn Trait по умолчаниюлишний overheadgenerics, если типы известны
clone() вместо borrowскрытые копии& / &mut

Пример структуры

Код ITЗагрузка примера кода…

Разбор:

  • struct Unit описывает данные персонажа, а impl Unit — его поведение (методы).
  • new() возвращает новый объект, damage() вычисляет урон, attack() изменяет цель через &mut Unit.
  • &self в damage означает "читать объект", &mut self в attack — "изменять объект".
  • В main два экземпляра (warrior, mage) взаимодействуют через методы, что иллюстрирует инкапсуляцию поведения.

Ключевое слово struct создаёт новый составной тип данных. Структура Unit объявляет набор именованных полей с указанием типа каждого поля. Тип String представляет владеющую строку в куче памяти. Тип i32 представляет 32-битное знаковое целое число. Поля по умолчанию приватны; для доступа из других модулей нужен префикс pub (в учебном примере ниже поля без pub — их меняют только методы в том же модуле).

Ключевое слово impl открывает блок реализации методов для структуры. Все методы, объявленные внутри блока, ассоциируются с типом структуры. Блок реализации группирует поведение, относящееся к конкретному типу данных.

Метод new создаёт и возвращает новый экземпляр структуры. Метод не принимает параметров и возвращает владение новым объектом. Конструктор использует синтаксис инициализации структуры с указанием значений для всех полей. Функция String::from преобразует строковый литерал в владеющую строку типа String.

Метод damage принимает неизменяемую ссылку &self на текущий экземпляр. Ключевое слово self является сокращением для self: &Self. Метод возвращает целочисленное значение, вычисленное на основе полей структуры. При каждом вызове метода происходит актуальный расчёт без сохранения промежуточного результата.

Метод attack принимает изменяемую ссылку &mut self на атакующий объект и изменяемую ссылку &mut Unit на цель. Изменяемые ссылки позволяют методу модифицировать состояние обоих объектов. Локальная переменная dmg сохраняет результат вычисления урона для повторного использования. Макрос println! форматирует и выводит текст в стандартный поток вывода. Оператор -= уменьшает здоровье цели на величину урона.

Rust применяет модель владения для управления памятью без сборщика мусора. Каждое значение имеет единственного владельца. При передаче значения в функцию или присваивании владение перемещается. Ссылки позволяют временно заимствовать значение без передачи владения. Изменяемые ссылки требуют уникального доступа к данным в конкретный момент времени.

Ключевое слово let mut объявляет изменяемую переменную. Только изменяемые переменные могут передаваться как изменяемые ссылки в методы. Вызов Unit::new() создаёт новый экземпляр и передаёт владение переменной. Последующие присваивания изменяют значения полей структуры напрямую.

Тип String представляет динамическую строку в куче памяти с владением данными. Строковые литералы имеют тип &str — срез строки со статическим временем жизни. Функция String::from создаёт владеющую строку из строкового литерала. Присваивание нового значения полю name заменяет предыдущую строку, автоматически освобождая память старой строки.

Функция main служит точкой входа для исполняемого приложения. Функция не принимает параметров и не возвращает значение явно. Все исполняемые инструкции программы размещаются внутри тела функции main. Компилятор автоматически вызывает эту функцию при запуске скомпилированного бинарного файла.

Исходный файл .rs компилируется в нативный бинарник. Для одиночного файла или проекта Cargo:

rustc main.rs
cargo run

Разбор:

  • rustc main.rs компилирует одиночный файл Rust напрямую в бинарник.
  • cargo run в проекте Cargo выполняет сборку и сразу запускает программу.
  • Во втором случае Cargo также управляет зависимостями, профилями и кэшем сборки.
  • Для учебных примеров полезно знать оба пути запуска.

Rust обеспечивает безопасность памяти на этапе компиляции, проверяя владение и заимствование без затрат во время выполнения.

Rust использует статическую типизацию с выводом типов. Компилятор выводит типы переменных на основе контекста использования. Целочисленные литералы требуют явного указания типа или выводятся из контекста операций. В safe Rust компилятор исключает use-after-free, data races и обращения к неинициализированной памяти; Option заменяет отсутствие значения вместо null-указателей.

Макрос println! принимает строку формата и аргументы для подстановки. Позиционные спецификаторы {} заменяются соответствующими аргументами в порядке их следования. Макрос компилируется в эффективный код вывода без динамического анализа формата во время выполнения. Макросы в Rust расширяются на этапе компиляции, обеспечивая безопасность и производительность.


Инкапсуляция

Интерактивная схема — инкапсуляция (псевдокод). Подробнее: Инкапсуляция.

Play ITЗагрузка интерактивного демо…

Инкапсуляция — это способность скрывать внутреннее устройство компонента и предоставлять только контролируемый интерфейс для взаимодействия с ним. В Rust инкапсуляция достигается за счёт системы модулей и правил видимости.

Структура в Rust может содержать поля, которые по умолчанию являются приватными. Это означает, что код вне модуля, в котором определена структура, не может напрямую читать или изменять эти поля. Для доступа к данным создаются публичные методы через блок impl. Такой подход гарантирует, что все изменения состояния происходят через строго определённые точки входа, что упрощает поддержку инвариантов и предотвращает некорректное использование данных.

Пример:

Код ITЗагрузка примера кода…

Разбор:

  • Поле balance приватное, поэтому внешний код не может менять его напрямую.
  • new задает начальное состояние счета и возвращает Self.
  • deposit и withdraw реализуют проверяемые изменения состояния через публичный API.
  • balance(&self) дает доступ только на чтение, сохраняя инварианты модели.

В этом примере поле balance скрыто от внешнего кода. Все операции с балансом проходят через методы, которые обеспечивают корректность бизнес-логики. Это полноценная реализация инкапсуляции без использования классов.

fn main() {
let mut account = BankAccount::new(100.0);
account.deposit(50.0);

if account.withdraw(30.0) {
println!("Списание успешно, баланс: {}", account.balance());
} else {
println!("Недостаточно средств");
}
}

Разбор:

  • BankAccount::new(100.0) создает счет через публичный конструктор.
  • deposit и withdraw изменяют состояние только через проверенные методы.
  • withdraw возвращает bool, поэтому вызывающий код явно обрабатывает успех/отказ.
  • Прямого доступа к balance нет: используется метод-геттер balance().

Полиморфизм

Интерактивная схема — полиморфизм (псевдокод). Подробнее: Полиморфизм.

Play ITЗагрузка интерактивного демо…

Полиморфизм — это возможность использовать один и тот же интерфейс для работы с разными типами данных. В Rust полиморфизм реализуется через трейты.

Трейт — это набор методов, которые могут быть реализованы для любого типа. Трейт определяет контракт: если тип реализует трейт, он обязан предоставить реализацию всех его методов. Это позволяет писать функции и структуры, которые работают с любыми типами, реализующими заданный трейт.

Например, можно определить трейт Drawable, который требует наличие метода draw:

pub trait Drawable {
fn draw(&self);
}

Разбор:

  • trait Drawable задает контракт поведения: любой реализующий тип обязан предоставить draw.
  • Метод принимает &self, поэтому рисование не требует изменения объекта.
  • Трейт отделяет интерфейс от конкретной реализации.
  • Это базовый кирпич полиморфизма в Rust вместо классового наследования.

Любой тип может реализовать этот трейт:

Код ITЗагрузка примера кода…

Разбор:

  • Определены два независимых типа (Circle, Square) с разными полями.
  • Оба типа реализуют один трейт Drawable, но с собственной логикой метода draw.
  • Одинаковый интерфейс и разные реализации — классический полиморфизм.
  • Такой подход масштабируется без глубокой иерархии классов.

Теперь можно написать функцию, которая принимает любой тип, реализующий Drawable:

pub fn render(shape: &dyn Drawable) {
shape.draw();
}

Разбор:

  • &dyn Drawable — объект трейта для динамической диспетчеризации во время выполнения.
  • Функция не знает конкретный тип, но гарантированно может вызвать draw().
  • Это удобно для гетерогенных коллекций и плагинных систем.
  • Цена — небольшой runtime-overhead на виртуальный вызов.

Это динамический полиморфизм, основанный на диспетчеризации во время выполнения. Rust также поддерживает статический полиморфизм через обобщённые типы и мономорфизацию, что позволяет избежать накладных расходов на вызовы виртуальных функций.

fn render_all(shapes: &[Box<dyn Drawable>]) {
for shape in shapes {
shape.draw();
}
}

fn main() {
let scene: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 3.0 }),
Box::new(Square { side: 4.0 }),
];
render_all(&scene);
}

Разбор:

  • &[Box<dyn Drawable>] — срез trait-объектов: разные типы, один интерфейс.
  • render_all не знает конкретный тип фигуры, только контракт Drawable.
  • Box нужен для хранения значений разного размера в одной коллекции.
  • Это динамический полиморфизм: нужный draw() выбирается во время выполнения.
fn print_area<T: Shape>(shape: &T) {
println!("Площадь: {}", shape.area());
}

Разбор:

  • T: Shape — ограничение trait bound: тип должен реализовать Shape.
  • Это статический полиморфизм: компилятор подставляет конкретную реализацию area().
  • Вызовы обычно быстрее dyn, потому что нет таблицы виртуальных методов.
  • Такой вариант предпочтителен, когда набор типов известен на этапе компиляции.

Отсутствие наследования

Rust не поддерживает наследование в том виде, в каком оно существует в классических ООП-языках. Наследование часто приводит к жёсткой связности между компонентами, усложняет рефакторинг и затрудняет понимание иерархий. Вместо этого Rust предлагает композицию и делегирование как основные способы повторного использования кода.

Композиция означает, что одна структура может содержать другие структуры в качестве полей. Делегирование — это передача части ответственности одной структуры другой. Например, вместо того чтобы наследовать от базового класса "Фигура", можно создать структуру, которая содержит общие свойства, и использовать её внутри других структур.

Код ITЗагрузка примера кода…

Разбор:

  • Position вынесена в отдельную структуру и переиспользуется по композиции.
  • Circle и Square "содержат" координаты, а не наследуются от базового класса.
  • Такой дизайн уменьшает связанность и облегчает изменение моделей данных.
  • Композиция здесь заменяет наследование "is-a" отношением "has-a".

Если необходимо повторно использовать поведение, можно вынести его в отдельный трейт и реализовать его для нужных типов. Такой подход более гибкий и соответствует принципу "предпочитай композицию наследованию".


Объектная безопасность и динамическая диспетчеризация

Для динамического полиморфизма (через dyn Trait) трейт должен быть объектно-безопасным: размер конкретного типа может быть неизвестен на этапе компиляции, и все методы трейта должны допускать вызов через vtable.

Трейт считается объектно-безопасным, если:

  • Он не содержит методов, возвращающих Self.
  • Он не содержит методов с обобщёнными параметрами.
  • Все его супер-трейты также объектно-безопасны.

Эти ограничения гарантируют, что таблица виртуальных функций может быть построена корректно, и вызовы методов будут работать независимо от конкретного типа.


Практические последствия

Отказ от классического ООП в пользу трейтов и структур даёт Rust ряд преимуществ. Безопасность памяти проверяется компилятором, без сборщика мусора и ручного free. Производительность предсказуема: виртуальные вызовы появляются только при явном dyn Trait. Композиция и трейты упрощают тестирование и рефакторинг.

В то же время Rust не запрещает использовать ООП-подход. Он просто предлагает альтернативные инструменты, которые лучше соответствуют целям языка: безопасности, производительности и выразительности. Разработчик может моделировать объекты, применять полиморфизм, инкапсулировать данные и строить сложные иерархии поведения — всё это без классов и наследования.


Моделирование поведения через трейты

В традиционных ООП-языках поведение часто инкапсулируется в иерархиях классов. Например, в Java можно определить абстрактный класс Shape с методом area(), а затем создать подклассы Circle, Rectangle и так далее. В Rust аналогичная задача решается через трейт:

Код ITЗагрузка примера кода…

Разбор:

  • Shape формализует общий интерфейс area() для разных геометрических фигур.
  • Circle и Rectangle реализуют контракт независимо, с разными формулами площади.
  • std::f64::consts::PI используется как стандартная константа числа пи.
  • Это пример статического полиморфизма на уровне трейтов.

Такой подход позволяет собирать коллекции из разных типов, реализующих один трейт:

let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 2.0 }),
Box::new(Rectangle { width: 3.0, height: 4.0 }),
];

for shape in &shapes {
println!("Area: {}", shape.area());
}

Разбор:

  • Vec<Box<dyn Shape>> хранит разные типы в одной коллекции через объект трейта.
  • Box нужен, потому что конкретные размеры Circle и Rectangle различаются.
  • Цикл вызывает area() полиморфно для каждого элемента.
  • Это практичный способ построить "список объектов с общим интерфейсом".

Это демонстрирует полиморфизм, но без жёсткой привязки к иерархии. Каждый тип остаётся независимым, и его поведение определяется только реализацией трейта.


Композиция вместо наследования

В Rust предпочтительным способом повторного использования кода является композиция. Логику координат выносят в структуру Position и включают её полем в другие типы, вместо базового класса PositionedObject:

Код ITЗагрузка примера кода…

Разбор:

  • derive(Debug, Clone) добавляет отладочный вывод и возможность копировать значение через clone().
  • Общая часть (Position) инкапсулируется как поле в разных типах объектов.
  • MovingObject и StaticObject используют одну и ту же геометрию без наследования.
  • Это демонстрация повторного использования через композицию и декларативные derive-макросы.

Если необходимо добавить поведение, связанное с позицией, можно определить трейт Locatable:

Код ITЗагрузка примера кода…

Разбор:

  • Locatable задает единый метод position() для доступа к координатам.
  • Обе реализации возвращают ссылку &Position, не передавая владение.
  • Функции, принимающие &dyn Locatable, смогут работать с обоими типами.
  • Контракт поведения отделен от конкретных структур, что упрощает расширение системы.

Теперь любая функция, принимающая &dyn Locatable, может работать с обоими типами, не зная об их внутреннем устройстве. Это гибче, чем наследование, поскольку не требует предварительного планирования иерархии.


Ассоциированные типы и обобщённые трейты

Rust позволяет делать трейты ещё более мощными за счёт ассоциированных типов. Это особенно полезно при проектировании интерфейсов, где тип возвращаемого значения зависит от конкретной реализации.

Пример — трейт для генератора случайных значений:

Код ITЗагрузка примера кода…

Разбор:

  • type Output; — ассоциированный тип: каждая реализация задает свой тип результата.
  • IntegerGenerator возвращает i32, StringGeneratorString.
  • Общий метод generate сохраняет единый интерфейс при разных выходных данных.
  • Такой паттерн делает API чище, чем трейт с множеством generic-параметров на каждом вызове.

Ассоциированные типы позволяют сохранять строгую типизацию без необходимости указывать обобщённые параметры при каждом вызове. Это делает API чище и безопаснее.


Трейты как контракты, не категории

Важно понимать, что трейты в Rust — это не просто набор методов. Они представляют собой контракты поведения. Если тип реализует трейт Display, он обязуется предоставлять человекочитаемое строковое представление. Если тип реализует Clone, он гарантирует возможность создания независимой копии. Эти контракты проверяются на этапе компиляции, что исключает ошибки времени выполнения, связанные с отсутствием ожидаемого поведения.

Это отличает Rust от динамических языков, где наличие метода проверяется только при вызове. В Rust компилятор знает всё о поведении каждого типа заранее.


Отсутствие множественного наследования — осознанный выбор

В языках с множественным наследованием (например, C++) часто возникает проблема "ромбовидного наследования", когда один и тот же метод наследуется по двум разным путям. Rust избегает этой проблемы, поскольку не имеет наследования вообще. Вместо этого вы можете реализовать несколько трейтов для одного типа:

Код ITЗагрузка примера кода…

Разбор:

  • Один тип Widget реализует сразу два независимых контракта: Drawable и Serializable.
  • Это заменяет множественное наследование через композицию поведений.
  • draw() отвечает за визуализацию, serialize() — за представление в строковом формате.
  • Поведение подключается явно и изолировано, без сложных иерархий базовых классов.

Такой подход даёт все преимущества множественного наследования без его сложностей.


Практические ограничения и компромиссы

Несмотря на гибкость, подход Rust к ООП требует от разработчика большего внимания к архитектуре. Нельзя быстро "наследовать от базового класса" и переопределить пару методов. Вместо этого нужно продумать, какие трейты нужны, как они взаимодействуют, и как обеспечить совместимость между компонентами.

Однако этот "недостаток" на практике превращается в преимущество: код становится более модульным, тестируемым и устойчивым к регрессиям. Отсутствие скрытых зависимостей между классами упрощает рефакторинг.


Учебные примеры ООП

Небольшие самодостаточные программы, которые показывают классы, объекты, инкапсуляцию, наследование и взаимодействие нескольких типов на одной предметной области.

Класс и объект

Чертёж класса Figure и конкретные объекты — круг и квадрат.

Код ITЗагрузка примера кода…


Банковский счёт

Инкапсуляция: скрытое поле баланса и методы deposit/withdraw.

Код ITЗагрузка примера кода…


Наследование

Родитель Animal и дочерние Cat и Dog с общим eat() и своим speak().

Код ITЗагрузка примера кода…


Смартфон

Состояние объекта: заряд батареи, звонки и подзарядка.

Код ITЗагрузка примера кода…


Студент

Список оценок, средний балл и проходной порог.

Код ITЗагрузка примера кода…


Корзина покупок

Взаимодействие Product, Cart и Order при оформлении заказа.

Код ITЗагрузка примера кода…


Автомобиль

Пробег, расход топлива и напоминание о техобслуживании.

Код ITЗагрузка примера кода…


Пользователь

Скрытый пароль, вход в систему и публикация сообщений.

Код ITЗагрузка примера кода…