Введение: 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