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

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

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

Начать курс

Введение: SOLID и Rust — идеальное сочетание

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

На протяжении статьи вы увидите, как Rust с его системой владения, трейтами и дженериками делает реализацию SOLID естественной и мощной. Мы начнем с Single Responsibility и закончим Dependency Inversion, предоставляя готовые к использованию примеры.

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

Суть: Каждый модуль, класс или функция должны иметь только одну причину для изменения. В Rust это означает, что структура должна отвечать за одну задачу.

Плохой пример (нарушение SRP)

struct Report {
    title: String,
    content: String,
}

impl Report {
    fn generate(&self) -> String {
        format!("# {}\n\n{}", self.title, self.content)
    }

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

    fn send_email(&self, recipient: &str) {
        println!("Sending email to {} with report", recipient);
    }
}

Здесь структура Report отвечает за генерацию, сохранение и отправку — три разных причины для изменения. Это нарушает SRP.

Хороший пример (соблюдение SRP)

struct Report {
    title: String,
    content: String,
}

impl Report {
    fn generate(&self) -> String {
        format!("# {}\n\n{}", self.title, self.content)
    }
}

struct ReportSaver;

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

struct EmailSender;

impl EmailSender {
    fn send(&self, recipient: &str, message: &str) {
        println!("Sending email to {}: {}", recipient, message);
    }
}

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

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

Суть: Программные сущности должны быть открыты для расширения, но закрыты для модификации. В Rust это достигается через трейты и дженерики.

Пример: система фильтрации

trait Filter<T> {
    fn apply(&self, items: &[T]) -> Vec<T>;
}

struct EvenFilter;

impl Filter<i32> for EvenFilter {
    fn apply(&self, items: &[i32]) -> Vec<i32> {
        items.iter().filter(|&x| x % 2 == 0).copied().collect()
    }
}

struct GreaterThanFilter {
    threshold: i32,
}

impl Filter<i32> for GreaterThanFilter {
    fn apply(&self, items: &[i32]) -> Vec<i32> {
        items.iter().filter(|&&x| x > self.threshold).copied().collect()
    }
}

fn process<T: Clone, F: Filter<T>>(items: &[T], filter: F) -> Vec<T> {
    filter.apply(items)
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];
    let even = process(&numbers, EvenFilter);
    let greater = process(&numbers, GreaterThanFilter { threshold: 3 });
    println!("Even: {:?}, Greater: {:?}", even, greater);
}

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

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

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

Пример: геометрические фигуры

trait Shape {
    fn area(&self) -> f64;
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

fn print_area(shape: &dyn Shape) {
    println!("Area: {}", shape.area());
}

fn main() {
    let rect = Rectangle { width: 3.0, height: 4.0 };
    let circle = Circle { radius: 2.0 };
    print_area(&rect);  // Работает
    print_area(&circle); // Работает
}

Обе структуры корректно реализуют Shape и могут быть переданы в print_area. Если бы Circle возвращал отрицательную площадь, это нарушило бы LSP.

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

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

Плохой пример (один большой трейт)

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

struct Human;
impl Worker for Human {
    fn work(&self) { println!("Working"); }
    fn eat(&self) { println!("Eating"); }
    fn sleep(&self) { println!("Sleeping"); }
}

struct Robot;
impl Worker for Robot {
    fn work(&self) { println!("Working"); }
    fn eat(&self) { panic!("Robot doesn't eat!"); } // Нарушение ISP
    fn sleep(&self) { println!("Charging"); }
}

Хороший пример (разделенные трейты)

trait Workable {
    fn work(&self);
}

trait Eatable {
    fn eat(&self);
}

trait Sleepable {
    fn sleep(&self);
}

struct Human;
impl Workable for Human { fn work(&self) { println!("Working"); } }
impl Eatable for Human { fn eat(&self) { println!("Eating"); } }
impl Sleepable for Human { fn                

Блоги

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