Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Clean Code Principles

"Чистый код" - это концепция, предложенная Робертом Мартином в его книге "Clean Code: A Handbook of Agile Software Craftsmanship". Принципы чистого кода призывают к написанию программного кода, который легко читаем, понятен, поддается тестированию и легко поддается изменениям. Вот некоторые из основных принципов чистого кода:

Ясность (Clarity)

Ясный код — это код, который невозможно понять неправильно.

Код должен быть максимально понятным и читаемым для других разработчиков, включая вас самих в будущем. Отдавайте предпочтение простым и очевидным решениям, даже если они кажутся «излишне примитивными». Простота снижает когнитивную нагрузку и уменьшает вероятность ошибок.

Ключевые моменты:

  • Выразительные имена переменных, функций, классов, модулей.
  • Отсутствие скрытого поведения — поведение должно быть очевидным из названия и структуры кода.
  • Минимум умных трюков — избегайте чрезмерно оптимизированных или изощрённых конструкций, если они ухудшают понимание.
  • Предсказуемость — одинаковый код должен вести себя одинаково в любых условиях.
Избегание магических чисел и строк:

Избегайте использования "магических" (хардкодированных) чисел и строк. Используйте константы или переменные с понятными именами.

Амбигуальный код (ambiguous code)

Амбигуальность в коде — это неоднозначность, когда программист не может однозначно понять, что именно произойдёт при его выполнении. Такой код сложнее сопровождать и он чаще становится источником ошибок.

Примеры неоднозначности:

  1. Неочевидные приоритеты операций
let result = a & b == 0; // Неочевидно: сравнение или побитовое И?
  1. Перекрытие переменных
let count = 10;
{
    let count = get_count(); // Затеняет внешнюю переменную
}
  1. Функции с разным поведением в зависимости от типа аргумента (особенно в языках с динамической типизацией или сложным overloading).
  2. Неявные преобразования типов (часто в JavaScript, C++ и других языках с implicit casting).
  3. Проблемы многопоточности — когда результат зависит от порядка выполнения потоков, который не гарантирован.
Правила дизайна
  1. Держите конфигурацию на верхних уровнях. Храните параметры, настройки и конфигурационные данные на высоких уровнях системы, чтобы менять поведение можно было без изменения низкоуровневого кода.

    • Плюс: гибкость и упрощённое сопровождение.
  2. Предпочитайте полиморфизм условным конструкциям. Избегайте нагромождения if/else или switch/case для выбора поведения. Используйте полиморфизм, общий интерфейс или абстрактный базовый класс.

    • Плюс: добавление нового поведения не требует изменения старого кода (Open/Closed Principle).
  3. Изолируйте многопоточность. Код, связанный с параллелизмом, должен быть отделён от бизнес-логики.

    • Плюс: снижение сложности, упрощение тестирования и уменьшение числа ошибок гонок данных.
  4. Избегайте чрезмерной конфигурируемости. Делайте настраиваемым только то, что действительно нужно изменять. Чрезмерная гибкость усложняет систему и делает её менее предсказуемой.

    • Баланс: между гибкостью и устойчивостью.
  5. Используйте внедрение зависимостей (Dependency Injection). Объект должен получать зависимости извне, а не создавать их сам.

    • Плюс: тестируемость, модульность, упрощённая замена зависимостей.
  6. Следуйте Закону Деметры (Law of Demeter). Общайтесь только со своими непосредственными зависимостями («не разговаривайте с незнакомцами»).

    • Плюс: снижение связанности и упрощение сопровождения.
Советы по понятности

Есть общее с KISS

  1. Будьте последовательны. Делайте однотипные вещи одинаково. Последовательность повышает предсказуемость и снижает нагрузку на понимание.

    // ❌ — разный стиль имён функций
    fn get_user() { }
    fn fetch_order() { }
    fn retrieve_product() { }
    
    // ✅ — единообразие
    fn get_user() { }
    fn get_order() { }
    fn get_product() { }
  2. Используйте поясняющие переменные. Явные и содержательные имена переменных делают код самодокументируемым.

    // ❌
    let s = "2024-07-20T12:34:56Z";
    let t = DateTime::parse_from_rfc3339(s).unwrap();
    
    // ✅
    let date_string = "2024-07-20T12:34:56Z";
    let parsed_date = DateTime::parse_from_rfc3339(date_string).unwrap();
  3. Инкапсулируйте граничные условия. Храните проверку диапазонов и пределов в одном месте — это снижает дублирование и ошибки.

    // ❌
    fn is_valid_age(age: u32) -> bool {
        age >= 0 && age <= 120
    }
    
    // ✅
    const MIN_AGE: u32 = 0;
    const MAX_AGE: u32 = 120;
    
    fn is_valid_age(age: u32) -> bool {
        age >= MIN_AGE && age <= MAX_AGE
    }
  4. Предпочитайте специализированные объекты значений примитивам. Используйте отдельные типы вместо "голых" примитивов, чтобы выразить смысл и избежать ошибок.

    struct Money { amount: f64 }
    
    impl Money {
        fn new(amount: f64) -> Self { Self { amount } }
        fn add(&self, other: Money) -> Money {
            Money::new(self.amount + other.amount)
        }
    }
    
    let price = Money::new(100.0);
    let tax = Money::new(20.0);
    let total = price.add(tax);
  5. Избегайте логической зависимости методов. Метод не должен работать правильно только при условии, что перед этим вызван другой метод.

    // ❌
    fn print_total(&self) {
        println!("{}", self.total); // требует, чтобы calculate_total был вызван ранее
    }
    
    // ✅
    fn print_total(&self) {
        println!("{}", self.calculate_total());
    }
  6. Избегайте отрицательных условий. Положительные проверки читаются легче, чем отрицательные.

    // ❌
    fn is_not_error(status: &str) -> bool {
        status != "error"
    }
    
    // ✅
    fn is_success(status: &str) -> bool {
        status == "success"
    }
Правила для функции

Правила для функции

  1. Делайте только одно. Функция должна решать одну задачу и решать её полностью. Это повышает понятность, упрощает тестирование и повторное использование.

    // ❌ — функция делает всё сразу
    fn process_order(order: &Order) {
       /* 
         code validate order 
         ...
       */
       /*
         code save to database  
         ...
       */
       /* 
         code send confirmation email 
         ...
       */
    }
    
    // ✅ — каждая функция делает одно
    fn validate_order(order: &Order) -> bool { /* ... */ }
    fn save_to_database(order: &Order) { /* ... */ }
    fn send_confirmation_email(order: &Order) { /* ... */ }
  2. Ограничивайте количество аргументов. Чем больше аргументов, тем сложнее понять и использовать функцию. Оптимально — до трёх аргументов. Если нужно больше — объедините их в структуру.

    // ❌
    fn create_user(name: &str, age: u32, email: &str, address: &str) { /* ... */ }
    
    // ✅ — используем объект для передачи данных
    struct User {
        name: String,
        age: u32,
        email: String,
        address: String,
    }
    fn create_user(user: User) { /* ... */ }
  3. Избегайте побочных эффектов. Функция не должна менять состояние вне своего тела или модифицировать аргументы неожиданным образом.

    // ❌ — меняет глобальное состояние
    static mut GLOBAL_STATE: HashMap<String, String> = HashMap::new();
    fn update_state(key: String, value: String) {
        unsafe { GLOBAL_STATE.insert(key, value); }
    }
    
    // ✅ — передаём состояние явно
    fn update_state(state: &mut HashMap<String, String>, key: String, value: String) {
        state.insert(key, value);
    }
  4. Не используйте флаговые аргументы. Если в функции есть флаг (true/false), это признак, что она делает больше одной задачи. Разделите её на две или более функций.

    // ❌
    fn set_user_status(user: &mut User, active: bool) {
        if active {
            user.status = "active".to_string();
        } else {
            user.status = "inactive".to_string();
        }
    }
    
    // ✅
    fn activate_user(user: &mut User) {
        user.status = "active".to_string();
    }
    fn deactivate_user(user: &mut User) {
        user.status = "inactive".to_string();
    }
Правила комментариев

Комментарии должны использоваться только там, где это действительно необходимо для понимания кода. Избегайте лишних или бессмысленных комментариев.

Главный принцип — комментарии нужны только тогда, когда смысл кода нельзя сделать очевидным самим кодом.

Маркер TODO

Используйте TODO для обозначения незавершённых задач, добавляя идентификатор задачи и ответственного. Это упрощает отслеживание.

// TODO [ISSUE-101] (Owner: Bob) Refactor this function
fn refactor_function() {
    // ...
}
  1. Объясняйте суть в коде, без необходимости обьяснять его в комментариях. Пишите код так, чтобы он был самодокументируемым.

    // ❌
    // Функция для проверки, является ли пользователь активным
    fn check(user: &User) -> bool {
        user.status == "active"
    }
    
    // ✅
    fn is_user_active(user: &User) -> bool {
        user.status == "active"
    }
  2. Не дублируйте очевидное. Избегайте «шума». Комментарий, который повторяет код, бесполезен.

    // ❌
    let x = x + 1; // Увеличиваем x на 1
    
    // ✅
    let x = x + 1;
  3. Не храните закомментированный код. Лишний код засоряет репозиторий. Если он не нужен — удалите его, история Git его сохранит. Если код "ценный" - просто сохраните в свое личное хранилище.

    // ❌
    // fn old_function() {
    //  // старый код
    // }
    
    // ✅
    fn new_function() {
        // новый код
    }
  4. Поясняйте намерения т.е. почему, а не что делает код.

    // ❌
    let result = calculate(); // Вызываем calculate
    
    // ✅
    // Кэшируем результат, чтобы избежать повторного вызова тяжелой функции
    let result = calculate();
  5. Документируйте сложные участки. Если логика нетривиальна — поясните её словами.

    // ❌
    let data = fetch_data();
    
    // ✅
    // Получаем данные из внешнего API и преобразуем их в формат JSON
    let data = fetch_data();
  6. Предупреждайте о последствиях. Если операция имеет побочные эффекты — напишите об этом.

    // ❌
    delete_user(user_id);
    
    // ✅
    // Удаление пользователя приведёт к удалению всех связанных данных
    delete_user(user_id);
Структура исходного кода

Структура исходного кода

  1. Разделяйте концепции вертикально:

    Разные концепции, уровни абстракции или функциональные блоки должны находиться в отдельных частях файла или модуля. Это делает код более организованным и читабельным. Не смешивайте бизнес-логику, валидацию, вспомогательные функции и т.д.

  2. Группировка кода

    • Связанный код должен быть размещён вместе.
    • Зависимые функции должны быть близко, для облегчения понимания их взаимодействия.
    • Похожие функции должны быть близко.
  3. Используйте пустые строки для разделения логической структуры, разделения слабо связанных частей.

    // ❌ Плохо
    let a = 1;
    let b = 2;
    let c = 3;
    if a > b {
        println!("a is greater than b");
    }
    let result = a + b + c;
    println!("Result: {}", result);
    
    // ✅ Хорошо
    let a = 1;
    let b = 2;
    let c = 3;
    
    if a > b {
        println!("a is greater than b");
    }
    
    let result = a + b + c;
    println!("Result: {}", result);

    Фигурные скобки {...} для изоляции области видимости переменных, и визуального выделения логической части кода.

    let a = 1;
    let b = {
        // какая-то своя логика ...
        let b = 0;
        a + b
    }
  4. Держите строки короткими:

    Короткие строки облегчают чтение кода и предотвращают горизонтальный скроллинг.

    // ❌
    let very_long_variable_name = "This is a very long string that exceeds the recommended line length for readability";
    // ✅
    let very_long_variable_name = "This is a very long string \
                                that exceeds the recommended \
                                line length for readability";
  5. Не используйте горизонтальное выравнивание и не нарушайте отступы:

    Горизонтальное выравнивание переменных и комментариев затрудняет внесение изменений и поддержание кода. Когда вы выравниваете переменные и комментарии по горизонтали, изменение одного элемента может потребовать перестановки остальных элементов на той же линии. И rustfmt всё равно его уберёт, а выравнивание вручную усложняет правки.

    // ❌
    let first_variable    = 1;    // Первый комментарий
    let second_variable   = 2;    // Второй комментарий
    
    // ✅
    let first_variable = 1;  // Первый комментарий
    let second_variable = 2; // Второй комментарий
Объекты и структуры данных

Объекты и структуры данных

  1. Упаковывайте внутреннее устройство в безопасный обьект

Приватность особенностей внутренней структуры обьекта, помогает избежать зависимостей от конкретных деталей реализации и делает ваш код более устойчивым к будущим изменениям.

// ❌ публичные поля структуры, позволяют использовать ее неверным способом  
pub struct Order {
    pub items: Vec<Item>,
    pub total: f64,
}

// ✅ струтура предоставляет безопасный способ работы с ней
pub struct Order {
    items: Vec<Item>,
    total: f64,
}
impl Order {
  pub fn new() -> Self {
      Order {
          items: Vec::new(),
          total: 0.0,
      }
  }
  pub fn add_item(&mut self, item: Item) {
      self.items.push(item);
      self.update_total();
  }
  pub fn get_total(&self) -> f64 {
      self.total
  }
  fn update_total(&mut self) {
      self.total = self.items.iter().map(|item| item.price).sum();
  }
}
  1. Предпочитайте структуры данных

Предпочитайте простые структуры данных без избыточного состояния и кэшей, чтобы уменьшить сложность и повысить ясность кода. Логика/поведение должна быть отделена от хранения данных, чтобы облегчить сопровождение и повысить читаемость кода. Избегайте дублирования данных и логики внутри структуры, поддерживайте единую точку истины. Гибридные структуры, которые смешивают поведение и данные, могут быть трудны для понимания и сопровождения. Стремитесь к тому, чтобы структура данных и объекты имели ясное разделение.

✅ структура хранит данные, методы просто оперируют с этими данными (чистые вычисления или простые изменения).

❌ структура хранит данные и дублирует вычисления и состояние.

pub struct Item {
    pub price: f64,
}

// ❌ хранение кэшированного состояния и его поддержка
pub struct Order {
    items: Vec<Item>,
    total: f64,  // состояние, требующее обновления вручную
}
impl Order {
    pub fn new() -> Self {
        Order {
            items: Vec::new(),
            total: 0.0,
        }
    }
    pub fn add_item(&mut self, item: Item) {
        self.items.push(item);
        self.total += item.price;  // риск рассинхронизации total и items
    }
    pub fn get_total(&self) -> f64 {
        self.total
    }
}

// ✅ хранение только данных, вычисление состояния на лету
pub struct Order {
    items: Vec<Item>,
}
impl Order {
    pub fn new() -> Self {
        Order {
            items: Vec::new(),
        }
    }
    pub fn add_item(&mut self, item: Item) {
        self.items.push(item);
    }
    pub fn calculate_total(&self) -> f64 {
        self.items.iter().map(|item| item.price).sum()
    }
}
  1. Объекты должны делать одно дело

Объекты и структуры данных должны быть сфокусированы на выполнении одной задачи.

  1. Базовый класс не должен знать о своих производных

В объектно-ориентированном программировании, базовый класс не должен иметь зависимости от своих производных классов.

В Rust вместо базовых классов и наследников используют трейты для описания поведения. Трейт не знает ничего о типах, которые его реализуют — это уже реализаторы (типы) знают про трейт, а не наоборот. Поэтому в Rust эта проблема (базовый класс, знающий о производных) просто не возникает.

Однако, есть случаи, которые можно рассматривать как «нарушение духа» этого принципа, например:

    1. Зависимость от конкретных типов в трейте через ассоциированные типы

Если в трейте есть ассоциированные типы (associated types) или связанные константы, и мы заставляем реализацию использовать определённые типы, то в некотором смысле трейт начинает иметь представление о типах, связанных с реализациями.

pub trait Animal {
    type Food;

    fn eat(&self, food: Self::Food);
}

Но даже здесь трейт просто определяет интерфейс с «параметром», а не конкретно зависит от реализации.

    1. Типажи с вызовом конкретных реализаций (в редких случаях)

Если в трейте сделать метод с дефолтной реализацией, который зависит от конкретного типа через downcasting или unsafe — это плохая практика и похожа на нарушение принципа. Впрочем, в стандартном Rust таких вещей не делают.

    1. Связь через замыкания, где трейт «знает» о конкретных типах через параметры

Иногда, когда трейт имеет метод, принимающий конкретный тип или замыкание с конкретным захватом, это создаёт более сильную связь, но опять же это касается конкретных вызовов, а не самого трейта.

  1. Лучше иметь много функций, чем передавать код в функцию для выбора поведения

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

// ❌ использование флага для выбора поведения
pub fn handle_order(order: &Order, use_discount: bool) {
    if use_discount {
        // Применение скидки
    } else {
        // Без скидки
    }
}
// ✅ использование отдельных функций, избежание флагов повышает читаемость и упрощает тестирование.
pub fn handle_order_with_discount(order: &Order) {
    // Применение скидки
}
pub fn handle_order_without_discount(order: &Order) {
    // Без скидки
}
  1. Предпочитайте нестатические методы статическим

Нестатические методы (&self/&mut self) позволяют работать с состоянием экземпляра, обеспечивают полиморфизм и расширяемость, в то время как статические методы не имеют доступа к состоянию и чаще используются как конструкторы или вспомогательные функции. В Rust это не строгий догмат, а рекомендация, чтобы делать код более идиоматичным и понятным.

Code smells

Code smells

Code smells — это признаки того, что код может содержать потенциальные проблемы или области для улучшения. Увеличивают когнитивную нагрузку на программиста.

  1. Rigidity (Жесткость)

Программное обеспечение становится трудным для изменения, поскольку небольшое изменение вызывает каскад последующих изменений. Это происходит из-за сильных зависимостей между компонентами системы.

// ❌ Order напрямую зависит от структуры Item, и добавление новых стратегий скидок, жёстко связано с ним.
struct Order {
    items: Vec<Item>,
    discount: f64,
}
impl Order {
    fn new() -> Self {
        Order {
            items: Vec::new(),
            discount: 0.0,
        }
    }
    fn apply_discount(&mut self) {
        let total = self.items.iter().map(|item| item.price).sum::<f64>();
        self.discount = total * 0.1;
    }
}
struct Item {
    price: f64,
}

// Изменение структуры Item потребует изменения в Order

✅ Как исправить:

Используйте абстракции, такие как трейты и интерфейсы, чтобы уменьшить взаимные зависимости.

#[derive(Debug)]
pub struct Item {
    pub price: f64,
}
pub trait DiscountStrategy {
    fn apply_discount(&self, total: f64) -> f64;
}
pub struct PercentageDiscount;

impl DiscountStrategy for PercentageDiscount {
    fn apply_discount(&self, total: f64) -> f64 {
        total * 0.1
    }
}
pub struct Order<T: DiscountStrategy> {
    items: Vec<Item>,
    discount_strategy: T,
}
impl<T: DiscountStrategy> Order<T> {
    pub fn new(discount_strategy: T) -> Self {
        Self {
            items: Vec::new(),
            discount_strategy,
        }
    }
    pub fn add_item(&mut self, item: Item) {
        self.items.push(item);
    }
    pub fn apply_discount(&self) -> f64 {
        let total: f64 = self.items.iter().map(|item| item.price).sum();
        self.discount_strategy.apply_discount(total)
    }
}
  1. Fragility (Хрупкость)

Программное обеспечение ломается в нескольких местах из-за одного изменения. Это часто вызвано слишком сильной связью между компонентами.

  // ❌ хрупкий код, у Order есть метод update_inventory, что связывает логику заказа с инвентарём.
  struct Order {
      items: Vec<Item>,
  }
  impl Order {
      fn add_item(&mut self, item: Item) {
          self.items.push(item);
          self.update_inventory();
      }

      fn update_inventory(&self) {
          // Логика обновления инвентаря
      }
  }
  struct Item {
      price: f64,
  }

✅ Как исправить:

Разделите ответственность между компонентами и используйте принципы инкапсуляции и декомпозиции. Чтобы избежать копирования Inventory при клонировании Order, стоит хранить ссылку или использовать Arc (в зависимости от контекста). Здесь для простоты — владение.

  // Inventory как отдельный компонент
  pub struct Inventory;  
  impl Inventory {
      pub fn update(&self) {
          // Логика обновления инвентаря
      }
  }
  pub struct Order {
      items: Vec<Item>,
      inventory: Inventory,
  }
  impl Order {
      pub fn new(inventory: Inventory) -> Self {
          Self {
              items: Vec::new(),
              inventory,
          }
      }
      pub fn add_item(&mut self, item: Item) {
          self.items.push(item);
          self.inventory.update();
      }
  }
  1. Immobility (Нет мобильности)

Невозможно повторно использовать части кода в других проектах из-за связанных рисков и высоких затрат.

  • В коде apply_discount жёстко зашит фиксированный способ скидки — 10% (магическое число 0.1).
  • Невозможно легко изменить логику скидки без правки самой структуры Order.
  • Из-за такой жёсткой связи сложно переиспользовать логику расчёта скидок в других местах или проектах — каждый раз придется копировать и менять код.
// ❌ сложно повторно использовать
struct Order {
    items: Vec<Item>,
}
impl Order {
    fn calculate_total(&self) -> f64 {
        self.items.iter().map(|item| item.price).sum()
    }
    fn apply_discount(&self) -> f64 {
        let total = self.calculate_total();
        total * 0.1
    }
}

✅ Как исправить:

Сделайте ваш код более модульным и независимым, выделяя общие компоненты в библиотеки.

  • Логика применения скидки вынесена в отдельный трейт DiscountStrategy, который можно реализовать любым способом.
  • Order не знает деталей, как именно считать скидку, а просто вызывает метод apply_discount из переданной стратегии.
  • Благодаря передаче стратегии через ссылку (&'a T) можно использовать разные варианты скидок без изменения структуры Order.
  • Такой код проще расширять, тестировать и переиспользовать в разных проектах — просто реализуй новую стратегию скидки, не меняя код заказа.
  • Это уменьшает связанность (coupling) и улучшает модульность.
  pub struct Item {
      pub price: f64,
  }
  pub trait DiscountStrategy {
      fn apply_discount(&self, total: f64) -> f64;
  }
  pub struct PercentageDiscount;
  impl DiscountStrategy for PercentageDiscount {
      fn apply_discount(&self, total: f64) -> f64 {
          total * 0.1
      }
  }
  // Используем жизненный цикл 'a, чтобы Order не владел стратегией скидки,
  // а только ссылался на нее — так можно использовать одну стратегию для разных заказов.
  pub struct Order<'a, T: DiscountStrategy> {
      pub items: Vec<Item>,
      discount_strategy: &'a T,
  }
  impl<'a, T: DiscountStrategy> Order<'a, T> {
      pub fn new(discount_strategy: &'a T) -> Self {
          Self {
              items: Vec::new(),
              discount_strategy,
          }
      }
      pub fn calculate_total(&self) -> f64 {
          self.items.iter().map(|item| item.price).sum()
      }
      pub fn apply_discount(&self) -> f64 {
          let total = self.calculate_total();
          self.discount_strategy.apply_discount(total)
      }
  }
  1. Needless Complexity (Избыточная сложность)

Код содержит ненужные усложнения, которые делают его трудным для понимания и поддержки.

// ❌ избыточная сложность
struct Order {
    items: Vec<Item>,
}
impl Order {
    fn calculate(&self, tax: f64, discount: f64) -> f64 {
        let subtotal = self.items.iter().map(|item| item.price).sum::<f64>();
        let taxed = subtotal * (1.0 + tax);
        let discounted = taxed - discount;
        discounted
    }
}

✅ Как исправить:

Разделите код на более простые и понятные части. Разбил метод calculate на subtotal и total — так понятнее и проще тестировать.

  pub struct Order {
      items: Vec<Item>,
  }
  impl Order {
      pub fn subtotal(&self) -> f64 {
          self.items.iter().map(|item| item.price).sum()
      }
      pub fn total(&self, tax: f64, discount: f64) -> f64 {
          let subtotal = self.subtotal();
          let taxed = subtotal * (1.0 + tax);
          taxed - discount
      }
  }
  1. Needless Repetition (Избыточное повторение)

Код содержит повторяющиеся фрагменты, что делает его сложным для поддержки и увеличивает вероятность ошибок.

// ❌ избыточное повторение
fn calculate_price(item: &Item, quantity: u32) -> f64 {
    item.price * quantity as f64
}
fn calculate_total(order: &Order) -> f64 {
    let mut total = 0.0;
    for item in &order.items {
        total += calculate_price(item, 1); // Количество всегда 1
    }
    total
}

✅ Как исправить:

Используйте общие функции и абстракции для устранения повторений. Упростил подсчёт цены, избавившись от цикла с мутабельной переменной.

  pub fn calculate_price(item: &Item, quantity: u32) -> f64 {
      item.price * quantity as f64
  }
  pub fn calculate_total(order: &Order) -> f64 {
      order.items.iter().map(|item| calculate_price(item, 1)).sum()
  }
  1. Opacity (Непрозрачность)

Код трудно понять из-за неясных или запутанных конструкций, что затрудняет его поддержку.

// ❌ непрозрачный код
fn process_data(data: &str) -> String {
    let mut result = String::new();
    for c in data.chars() {
        if c.is_alphabetic() {
            result.push(c.to_ascii_uppercase());
        } else if c.is_numeric() {
            result.push('0');
        }
    }
    result
}

✅ Как исправить:

Упрощайте код и добавляйте комментарии, чтобы сделать его более понятным.

  pub fn process_data(data: &str) -> String {
      let mut result = String::new();

      for c in data.chars() {
          if c.is_alphabetic() {
              result.push(c.to_ascii_uppercase());
          } else if c.is_numeric() {
              result.push('0'); // Заменяем цифры на символ '0'
          }
          // Другие символы игнорируются
      }
      result
  }
Code smells по Фаулеру

Этот список — не догма, а скорее набор ориентиров, которые помогают находить слабые места в коде и понимать, какие рефакторинги можно применить для их исправления.

Признаки, связанные с методами (функциями)

  1. Длинный метод (Long Method)

    • Что это: Метод, который слишком длинный и делает слишком много вещей.
    • Почему это плохо: Снижает читаемость, усложняет понимание и повторное использование.
  2. Большой класс (Large Class)

    • Что это: Класс, который пытается делать слишком много и содержит множество полей, методов и ответственностей.
    • Почему это плохо: Нарушает принцип единственной ответственности (SRP), становится "богом-объектом".
  3. Цепочка вызовов (Message Chains)

    • Что это: Длинные последовательности вызовов вида a.getB().getC().getD().doSomething().
    • Почему это плохо: Создает сильную связь между клиентом и структурой объектов. Изменение в одном звене цепи сломает весь код.
  4. Завышенные права (Feature Envy)

    • Что это: Метод одного класса чрезмерно часто использует данные и методы другого класса.
    • Почему это плохо: Показывает, что метод, скорее всего, должен находиться в том классе, к данным которого он так часто обращается.
  5. Интимные отношения (Inappropriate Intimacy)

    • Что это: Классы слишком много знают о внутреннем устройстве друг друга (часто через доступ к приватным полям/методам).
    • Почему это плохо: Сильная связность, изменение одного класса почти всегда ведет к изменению другого.
  6. Расходящиеся модификации (Divergent Change)

    • Что это: Когда один класс приходится часто менять по разным причинам (например, при изменении в БД и при изменении в формате отчета).
    • Почему это плохо: Нарушение SRP. Класс должен меняться только по одной причине.
  7. Стрельба дробью (Shotgun Surgery)

    • Что это: Противоположность "Расходящимся модификациям". Одно изменение в системе требует внесения множества мелких правок в множество классов.
    • Почему это плохо: Сложно отследить все места для изменений, легко что-то упустить.
  8. Парализующий страх перед добавлением метода (Parallel Inheritance Hierarchies)

    • Что это: Частный случай "Стрельбы дробью". При добавлении подкласса в одну иерархию, вам приходится добавлять подкласс и в другую, связанную с ней иерархию.

Признаки, связанные с данными

  1. Длинный список параметров (Long Parameter List)

    • Что это: Метод с большим количеством параметров.
    • Почему это плохо: Трудно понять и использовать, легко перепутать порядок аргументов.
  2. Группа данных (Data Clumps)

    • Что это: Несколько полей (например, String country, String city, String street) или параметров, которые всегда передаются вместе.
    • Почему это плохо: Лучше объединить их в отдельный класс-структуру (например, Address).
  3. Примитивная мания (Primitive Obsession)

    • Что это: Использование примитивных типов (строк, чисел) вместо маленьких объектов для представления доменных понятий (например, String phone вместо класса PhoneNumber).
    • Почему это плохо: Упускается возможность поместить поведение, связанное с этими данными, в один класс.
  4. Отклоняющийся код (Speculative Generality)

    • Что это: Наличие классов, методов или полей, которые "на всякий случай" добавлены для будущей функциональности, которая еще не нужна (YAGNI - You Ain't Gonna Need It).
    • Почему это плохо: Бесполезно усложняет код.

Признаки, связанные с наследованием

  1. Отказ от наследства (Refused Bequest)

    • Что это: Подкласс использует только часть унаследованных методов и свойств родителя.
    • Почему это плохо: Нарушает принцип подстановки Барбары Лисков (LSP). Часто лучше использовать композицию вместо наследования.
  2. Дублирование кода (Duplicate Code)

    • Что это: Один и тот же или очень похожий код встречается в нескольких местах.
    • Почему это плохо: Главный враг сопровождения. Исправление ошибки требует изменений в нескольких местах.
  3. Временное поле (Temporary Field)

    • Что это: Поле в классе, которое заполняется и используется только в определенных scenarios (часто в одном методе), а в остальное время равно null или не имеет смысла.
    • Почему это плохо: Сбивает с толку, так как ожидается, что поля объекта всегда имеют осмысленные значения.

Общие признаки и запахи дизайна

  1. Комментарии (Comments)

    • Важно: Фаулер не против комментариев вообще. "Запахом" являются комментарии, которые маскируют плохой код. Часто лучше отрефакторить код, чтобы он стал самодокументируемым, чем писать поясняющий комментарий.
  2. Ленивый класс (Lazy Class)

    • Что это: Класс, который слишком мал и не несет достаточной пользы, чтобы оправдать свое существование (например, после рефакторинга он стал почти пустым).
    • Почему это плохо: Бесполезная сложность.
  3. Данные-классы (Data Class)

    • Что это: Класс, который содержит только поля и геттеры/сеттеры, без какой-либо бизнес-логики.
    • Почями это плохо: Поведение, связанное с этими данными, размазано по другим классам, что приводит к "Завышенным правам".
  4. Свистопляска с операторами switch (Switch Statements)

    • Что это: Длинные или повторяющиеся операторы switch (или if/else if).
    • Почему это плохо: Часто нарушают OCP (Принцип открытости/закрытости). Добавление нового case требует модификации существующего кода. Решение — полиморфизм.
  5. Игнорирование исключений (Ignored Exceptions)

    • Что это: Пустой блок catch.
    • Почему это плохо: Ошибка подавляется, программа продолжает работать в неконсистентном состоянии, что приводит к непредсказуемым последствиям.
  6. Альтернативные классы с разными интерфейсами (Alternative Classes with Different Interfaces)

    • Что это: Два класса выполняют схожую функцию, но имеют разные имена методов или сигнатуры.
    • Почему это плохо: Запутывает и усложняет выбор правильного класса.
  7. Недостоверные имена (Mysterious Name)

    • Что это: Имена переменных, методов или классов, которые неясно отражают их назначение.
    • Почему это плохо: Код становится нечитаемым. Хорошее именование — одна из самых важных вещей в программировании.

  • Минимизация повторений (DRY - Don't Repeat Yourself):

    Избегайте дублирования кода. Если у вас есть повторяющийся код, вынесите его в отдельную функцию, метод или класс.

  • Маленькие функции (Small Functions):

    Функции должны быть небольшими и выполнять одну четко определенную задачу. Если функция становится слишком большой, разделите ее на более мелкие функции с понятными именами.

  • Принцип единственной ответственности (Single Responsibility Principle - SRP):

    Каждый класс или функция должны быть ответственными только за одну вещь. Это облегчает понимание и изменение кода.

  • Принцип открытости/закрытости (Open/Closed Principle - OCP):

    Код должен быть открыт для расширения, но закрыт для модификации. Это достигается путем использования абстракций и полиморфизма.

  • Тестирование:

    Пишите тесты для вашего кода, чтобы обеспечить его корректность и устойчивость к изменениям. Следуйте принципу "Тестирование приводит к чистому коду". Тесты заставляют думать о дизайне — чтобы код было легко тестировать, он обычно становится более модульным, с хорошо отделёнными зонами ответственности.

    • Раннее обнаружение ошибок — тесты выявляют баги сразу, предотвращая накопление сложных проблем.
    • Документация поведения — тесты служат живым описанием того, как должен работать код.
    • Упрощение рефакторинга — с тестами можно смело менять структуру кода, не боясь сломать функциональность.
    • Меньше избыточной сложности и дублирования — чтобы тесты были простыми, код обычно становится проще и чище.
  • Если нашли место для рефакторинга, следует это рефакторить:

    При внесении изменений в код, следите за тем, чтобы код оставался чистым или становился чище, чем был до ваших изменений.

    Правило бойскаута - "Оставьте место стоянки чище, чем оно было до вас". Правило бойскаута помогает избежать накопления технического долга и поддерживать кодовую базу в хорошем состоянии без лишних усилий.