SOLID принципы в Rust: теория и практика для разработчиков

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

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

Начать курс

Введение: SOLID принципы в Rust — зачем это нужно?

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

В этой статье мы разберем каждый из пяти принципов SOLID на практических примерах на Rust. Вы увидите, как trait заменяет интерфейсы, как Box<dyn Trait> помогает с инверсией зависимостей, и как избегать типичных ошибок.

1. Принцип единственной ответственности (SRP) в Rust

Single Responsibility Principle (SRP): каждый модуль, структура или функция должны иметь только одну причину для изменения. В Rust это означает, что структура должна отвечать за одну сущность, а не смешивать логику бизнеса, работу с данными и вывод.

Пример нарушения SRP

struct User {
    name: String,
    email: String,
}

impl User {
    // Смешиваем бизнес-логику и сохранение
    fn save_to_db(&self) {
        println!("Сохранение {} в БД", self.name);
    }
    
    fn send_email(&self, message: &str) {
        println!("Отправка email на {}: {}", self.email, message);
    }
}

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

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

struct User {
    name: String,
    email: String,
}

struct UserRepository;
impl UserRepository {
    fn save(&self, user: &User) {
        println!("Сохранение {} в БД", user.name);
    }
}

struct EmailService;
impl EmailService {
    fn send(&self, user: &User, message: &str) {
        println!("Отправка email на {}: {}", user.email, message);
    }
}

Теперь каждая структура имеет одну ответственность: User — данные, UserRepository — хранение, EmailService — отправка. Код стал более модульным и тестируемым.

2. Принцип открытости/закрытости (OCP) через типажи

Open/Closed Principle (OCP): программные сущности должны быть открыты для расширения, но закрыты для модификации. В Rust это идеально реализуется с помощью trait и динамической диспетчеризации.

Пример без OCP

fn calculate_area(shape: &str, params: &[f64]) -> f64 {
    match shape {
        "circle" => std::f64::consts::PI * params[0] * params[0],
        "rectangle" => params[0] * params[1],
        _ => panic!("Неизвестная фигура"),
    }
}

При добавлении новой фигуры (например, треугольника) придется модифицировать функцию.

Правильный подход с OCP

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

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

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

// Функция закрыта для модификации, но открыта для расширения
fn print_area(shape: &dyn Shape) {
    println!("Площадь: {}", shape.area());
}

Теперь для добавления треугольника достаточно создать новую структуру и реализовать trait Shape. Функция print_area не требует изменений.

3. Принцип подстановки Лисков (LSP) и наследование через типажи

Liskov Substitution Principle (LSP): объекты в программе должны быть заменяемы экземплярами их подтипов без изменения корректности программы. В Rust LSP обеспечивается корректной реализацией trait: подтип (реализация типажа) должен полностью удовлетворять контракту базового типа.

Пример нарушения LSP

trait Bird {
    fn fly(&self);
}

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

Здесь Penguin нарушает LSP, потому что вызывает панику при вызове fly(). Это делает замену некорректной.

Правильное решение

trait Bird {
    fn move_(&self);
}

trait FlyingBird: Bird {
    fn fly(&self);
}

struct Penguin;
impl Bird for Penguin {
    fn move_(&self) {
        println!("Пингвин плавает");
    }
}

struct Sparrow;
impl Bird for Sparrow {
    fn move_(&self) {
        println!("Воробей прыгает");
    }
}
impl FlyingBird for Sparrow {
    fn fly(&self) {
        println!("Воробей летит");
    }
}

Теперь Penguin не реализует FlyingBird, и мы не можем случайно вызвать для него fly(). Это корректное применение LSP.

4. Принцип разделения интерфейсов (ISP)

Interface Segregation Principle (ISP): клиенты не должны зависеть от методов, которые они не используют. В Rust это означает, что типажи должны быть небольшими и специализированными.

Пример нарушения ISP

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

struct Robot;
impl Worker for Robot {
    fn work(&self) { println!("Робот работает"); }
    fn eat(&self) { panic!("Робот не ест"); }
}

Робот вынужден реализовывать метод eat(), который ему не нужен.

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

trait Workable {
    fn work(&self);
}

trait Eatable {
    fn eat(&self);
}

struct Human;
impl Workable for Human { fn work(&self) { println!("Человек работает"); } }
impl Eatable for Human { fn eat(&self) { println!("Человек ест"); } }

struct Robot;
impl Workable for Robot { fn work(&                

Блоги

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