SOLID принципы в Rust: теория и практика

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему SOLID важен для Rust разработчиков

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

Вы узнаете, как:

  • Избегать "божественных" структур с помощью декомпозиции
  • Проектировать расширяемые системы через traits
  • Эффективно использовать полиморфизм без наследования
  • Разделять интерфейсы и внедрять зависимости

1. Single Responsibility Principle (SRP) — Принцип единственной ответственности

SRP гласит: "У структуры или модуля должна быть только одна причина для изменения". В Rust это означает, что каждый тип должен отвечать за одну четко определенную задачу.

Нарушение SRP: "Божественная" структура

struct Report {
    data: Vec<String>,
}

impl Report {
    fn generate(&self) -> String {
        self.data.join(", ")
    }

    fn save_to_file(&self, path: &str) -> std::io::Result<()> {
        let content = self.generate();
        std::fs::write(path, content)
    }

    fn send_email(&self, recipient: &str) {
        // Логика отправки email
        println!("Отправка отчета {} на {}", self.generate(), recipient);
    }
}

Этот код нарушает SRP: структура Report отвечает за генерацию данных, их сохранение в файл и отправку email. Изменение логики сохранения или отправки потребует изменения самого отчета.

Правильная реализация с SRP

struct Report {
    data: Vec<String>,
}

impl Report {
    fn new(data: Vec<String>) -> Self {
        Self { data }
    }

    fn generate(&self) -> String {
        self.data.join(", ")
    }
}

struct FileSaver;

impl FileSaver {
    fn save(report: &Report, path: &str) -> std::io::Result<()> {
        std::fs::write(path, report.generate())
    }
}

struct EmailSender;

impl EmailSender {
    fn send(report: &Report, recipient: &str) {
        println!("Отправка отчета {} на {}", report.generate(), recipient);
    }
}

Теперь каждая структура имеет одну ответственность: Report — генерация, FileSaver — сохранение, EmailSender — отправка. Это упрощает тестирование и изменение кода.

2. Open/Closed Principle (OCP) — Принцип открытости/закрытости

OCP утверждает: "Классы должны быть открыты для расширения, но закрыты для модификации". В Rust это достигается через traitы и обобщения.

Пример: Система обработки платежей

trait PaymentProcessor {
    fn process(&self, amount: f64) -> String;
}

struct CreditCardProcessor;

impl PaymentProcessor for CreditCardProcessor {
    fn process(&self, amount: f64) -> String {
        format!("Обработка {} через кредитную карту", amount)
    }
}

struct PayPalProcessor;

impl PaymentProcessor for PayPalProcessor {
    fn process(&self, amount: f64) -> String {
        format!("Обработка {} через PayPal", amount)
    }
}

struct PaymentHandler<T: PaymentProcessor> {
    processor: T,
}

impl<T: PaymentProcessor> PaymentHandler<T> {
    fn execute(&self, amount: f64) -> String {
        self.processor.process(amount)
    }
}

Добавление нового способа оплаты (например, CryptoProcessor) не требует изменения существующего кода — достаточно реализовать trait PaymentProcessor.

3. Liskov Substitution Principle (LSP) — Принцип подстановки Лисков

LSP требует, чтобы объекты подтипов могли заменять объекты базового типа без нарушения работы программы. В Rust это означает, что реализация trait должна соответствовать его контракту.

Нарушение LSP

trait Bird {
    fn fly(&self) -> String;
}

struct Sparrow;

impl Bird for Sparrow {
    fn fly(&self) -> String {
        "Воробей летит".to_string()
    }
}

struct Penguin;

impl Bird for Penguin {
    fn fly(&self) -> String {
        panic!("Пингвины не летают!");
    }
}

Пингвин не может летать, но реализует trait Bird. Это нарушает LSP: код, ожидающий, что все птицы летают, сломается.

Правильная реализация

trait Flyer {
    fn fly(&self) -> String;
}

trait Swimmer {
    fn swim(&self) -> String;
}

struct Sparrow;

impl Flyer for Sparrow {
    fn fly(&self) -> String {
        "Воробей летит".to_string()
    }
}

struct Penguin;

impl Swimmer for Penguin {
    fn swim(&self) -> String {
        "Пингвин плавает".to_string()
    }
}

Разделение на узкие traits (Flyer, Swimmer) гарантирует, что каждый тип реализует только те возможности, которые ему присущи.

4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса

ISP гласит: "Клиенты не должны зависеть от методов, которые они не используют". В Rust это означает создание маленьких, специфичных traits вместо одного большого.

Нарушение ISP

trait Worker {
    fn work(&self) -> String;
    fn eat(&self) -> String;
    fn sleep(&self) -> String;
}

struct Human;

impl Worker for Human {
    fn work(&self) -> String { "Работает".to_string() }
    fn eat(&self) -> String { "Ест".to_string() }
    fn sleep(&self) -> String { "Спит".to_string() }
}

struct Robot;

impl Worker for Robot {
    fn work(&self) -> String { "Работает 24/7".to_string() }
    fn eat(&self) -> String { panic!("Роботы не едят!") }
    fn sleep(&self) -> String { panic!("Роботы                

Блоги

Рекомендаций по книгам