Объектно-ориентированные концепции в Rust
Если ООП для вас новое, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделе — зачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.
Ниже — 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 |
|---|---|---|
| Класс | class | struct + impl |
| Наследование | extends | композиция + трейты |
| Интерфейс | interface | trait |
| Полиморфизм runtime | virtual | dyn Trait |
| Полиморфизм compile-time | generics | impl Trait, <T: Trait> |
| Инкапсуляция | модификаторы | модули + приватные поля |
| Конструктор | new Type() | Type::new() / литерал |
Типичные ошибки при переносе мышления из Java
| Ошибка | Почему плохо | Идиома Rust |
|---|---|---|
Глубокая иерархия struct | нет subtyping для struct | трейты + композиция |
| Публичные поля для DTO | ломают инварианты | pub только когда нужно, иначе методы |
Rc<RefCell<T>> везде | имитация GC | спроектировать владение |
dyn Trait по умолчанию | лишний overhead | generics, если типы известны |
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,StringGenerator—String.- Общий метод
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Загрузка примера кода…