SRP

Принцип

У каждого класса/модуля/функции должна быть только одна причина для изменения.

Пример из жизни: кофемашина. Если она варит кофе, сама себя чистит, сама заказывает зёрна через интернет, печатает чеки - то это плохо! Если изменится способ печати чеков - придётся менять кофемашину. Если поставщик зёрен поменяет API - снова менять кофемашину. Правильно: кофемашина только варит кофе. Чисткой занимается отдельный сотрудник, заказом зёрен — менеджер, печатью чеков — кассовый аппарат.

Пример антипаттерна в коде: SmartDevice::print_state() и хранит температуру, и выводит её в консоль:

impl SmartDevice {
    pub fn print_state(&self) {
        println!("Текущая температура: {:.1}°C", ...);  // привязка к stdout
    }
}

Если потом потребуется писать отчёты в файл, отправлять по сети, выводить на веб-страницу, логировать в JSON, то придётся менять сам доменный тип, и это плохо! ПравильноSmartDevice только хранит данные и отвечает за свою бизнес-логику. А за вывод отвечает кто-то другой.

Антипаттерн без Display:

struct Person {
    name: String,
    age: u8,
}

impl Person {
/// SRP нарушен: тип сам знает, как выводить в консоль
    pub fn print_info(&self) {
        println!("Person: {} Age: {}", self.name, self.age);
    }
}

fn main() {
    let person = Person {
        name: "Alex".into(),
        age: 20,
    };

    person.print_info(); // вывод только в консоль 
}

Паттерн SRP с Display:

use std::fmt;
struct Person {
    name: String,
    age: u8,
}

impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.name, self.age)
    }
}

fn main() {
    let person = Person {
        name: "Alex".into(),
        age: 20,
    };

    println!("{}", person); // вывод в консоль
    let text = format!("{}", person); // вывод в строку
    std::fs::write("out.txt", text).unwrap(); // вывод в файл
}

Что такое f: &mut fmt::Formatter<'_>:

  • Содержание (self - то, что надо “напечатать”)
  • Лист бумаги (f - куда Вы это записываете) fmt::Formatter - это как лист бумаги + ручка в одном флаконе. Он знает:
  • куда писать (в консоль, в строку, в файл)
  • какие настройки форматирования (ширина, выравнивание и т.д.)

Что происходит в println:

  1. println! создаёт свой Formatter, который указывает на стандартный вывод
  2. Вызывает person.fmt(&mut formatter)
  3. Метод fmt пишет в этот formatter
  4. println! добавляет перевод строки и отправляет в консоль