Clean Code Principles
"Чистый код" - это концепция, предложенная Робертом Мартином в его книге "Clean Code: A Handbook of Agile Software Craftsmanship". Принципы чистого кода призывают к написанию программного кода, который легко читаем, понятен, поддается тестированию и легко поддается изменениям. Вот некоторые из основных принципов чистого кода:
Ясность (Clarity)
Ясный код — это код, который невозможно понять неправильно.
Код должен быть максимально понятным и читаемым для других разработчиков, включая вас самих в будущем. Отдавайте предпочтение простым и очевидным решениям, даже если они кажутся «излишне примитивными». Простота снижает когнитивную нагрузку и уменьшает вероятность ошибок.
Ключевые моменты:
- Выразительные имена переменных, функций, классов, модулей.
- Отсутствие скрытого поведения — поведение должно быть очевидным из названия и структуры кода.
- Минимум умных трюков — избегайте чрезмерно оптимизированных или изощрённых конструкций, если они ухудшают понимание.
- Предсказуемость — одинаковый код должен вести себя одинаково в любых условиях.
Избегание магических чисел и строк:
Избегайте использования "магических" (хардкодированных) чисел и строк. Используйте константы или переменные с понятными именами.
Амбигуальный код (ambiguous code)
Амбигуальность в коде — это неоднозначность, когда программист не может однозначно понять, что именно произойдёт при его выполнении. Такой код сложнее сопровождать и он чаще становится источником ошибок.
Примеры неоднозначности:
- Неочевидные приоритеты операций
let result = a & b == 0; // Неочевидно: сравнение или побитовое И?
- Перекрытие переменных
let count = 10; { let count = get_count(); // Затеняет внешнюю переменную }
- Функции с разным поведением в зависимости от типа аргумента (особенно в языках с динамической типизацией или сложным overloading).
- Неявные преобразования типов (часто в JavaScript, C++ и других языках с implicit casting).
- Проблемы многопоточности — когда результат зависит от порядка выполнения потоков, который не гарантирован.
Правила дизайна
-
Держите конфигурацию на верхних уровнях. Храните параметры, настройки и конфигурационные данные на высоких уровнях системы, чтобы менять поведение можно было без изменения низкоуровневого кода.
- Плюс: гибкость и упрощённое сопровождение.
-
Предпочитайте полиморфизм условным конструкциям. Избегайте нагромождения
if/elseилиswitch/caseдля выбора поведения. Используйте полиморфизм, общий интерфейс или абстрактный базовый класс.- Плюс: добавление нового поведения не требует изменения старого кода (Open/Closed Principle).
-
Изолируйте многопоточность. Код, связанный с параллелизмом, должен быть отделён от бизнес-логики.
- Плюс: снижение сложности, упрощение тестирования и уменьшение числа ошибок гонок данных.
-
Избегайте чрезмерной конфигурируемости. Делайте настраиваемым только то, что действительно нужно изменять. Чрезмерная гибкость усложняет систему и делает её менее предсказуемой.
- Баланс: между гибкостью и устойчивостью.
-
Используйте внедрение зависимостей (Dependency Injection). Объект должен получать зависимости извне, а не создавать их сам.
- Плюс: тестируемость, модульность, упрощённая замена зависимостей.
-
Следуйте Закону Деметры (Law of Demeter). Общайтесь только со своими непосредственными зависимостями («не разговаривайте с незнакомцами»).
- Плюс: снижение связанности и упрощение сопровождения.
Советы по понятности
Есть общее с KISS
-
Будьте последовательны. Делайте однотипные вещи одинаково. Последовательность повышает предсказуемость и снижает нагрузку на понимание.
// ❌ — разный стиль имён функций fn get_user() { } fn fetch_order() { } fn retrieve_product() { } // ✅ — единообразие fn get_user() { } fn get_order() { } fn get_product() { } -
Используйте поясняющие переменные. Явные и содержательные имена переменных делают код самодокументируемым.
// ❌ 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(); -
Инкапсулируйте граничные условия. Храните проверку диапазонов и пределов в одном месте — это снижает дублирование и ошибки.
// ❌ 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 } -
Предпочитайте специализированные объекты значений примитивам. Используйте отдельные типы вместо "голых" примитивов, чтобы выразить смысл и избежать ошибок.
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); -
Избегайте логической зависимости методов. Метод не должен работать правильно только при условии, что перед этим вызван другой метод.
// ❌ fn print_total(&self) { println!("{}", self.total); // требует, чтобы calculate_total был вызван ранее } // ✅ fn print_total(&self) { println!("{}", self.calculate_total()); } -
Избегайте отрицательных условий. Положительные проверки читаются легче, чем отрицательные.
// ❌ fn is_not_error(status: &str) -> bool { status != "error" } // ✅ fn is_success(status: &str) -> bool { status == "success" }
Правила для функции
Правила для функции
-
Делайте только одно. Функция должна решать одну задачу и решать её полностью. Это повышает понятность, упрощает тестирование и повторное использование.
// ❌ — функция делает всё сразу 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) { /* ... */ } -
Ограничивайте количество аргументов. Чем больше аргументов, тем сложнее понять и использовать функцию. Оптимально — до трёх аргументов. Если нужно больше — объедините их в структуру.
// ❌ 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) { /* ... */ } -
Избегайте побочных эффектов. Функция не должна менять состояние вне своего тела или модифицировать аргументы неожиданным образом.
// ❌ — меняет глобальное состояние 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); } -
Не используйте флаговые аргументы. Если в функции есть флаг (
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() { // ... }
-
Объясняйте суть в коде, без необходимости обьяснять его в комментариях. Пишите код так, чтобы он был самодокументируемым.
// ❌ // Функция для проверки, является ли пользователь активным fn check(user: &User) -> bool { user.status == "active" } // ✅ fn is_user_active(user: &User) -> bool { user.status == "active" } -
Не дублируйте очевидное. Избегайте «шума». Комментарий, который повторяет код, бесполезен.
// ❌ let x = x + 1; // Увеличиваем x на 1 // ✅ let x = x + 1; -
Не храните закомментированный код. Лишний код засоряет репозиторий. Если он не нужен — удалите его, история Git его сохранит. Если код "ценный" - просто сохраните в свое личное хранилище.
// ❌ // fn old_function() { // // старый код // } // ✅ fn new_function() { // новый код } -
Поясняйте намерения т.е. почему, а не что делает код.
// ❌ let result = calculate(); // Вызываем calculate // ✅ // Кэшируем результат, чтобы избежать повторного вызова тяжелой функции let result = calculate(); -
Документируйте сложные участки. Если логика нетривиальна — поясните её словами.
// ❌ let data = fetch_data(); // ✅ // Получаем данные из внешнего API и преобразуем их в формат JSON let data = fetch_data(); -
Предупреждайте о последствиях. Если операция имеет побочные эффекты — напишите об этом.
// ❌ delete_user(user_id); // ✅ // Удаление пользователя приведёт к удалению всех связанных данных delete_user(user_id);
Структура исходного кода
Структура исходного кода
-
Разделяйте концепции вертикально:
Разные концепции, уровни абстракции или функциональные блоки должны находиться в отдельных частях файла или модуля. Это делает код более организованным и читабельным. Не смешивайте бизнес-логику, валидацию, вспомогательные функции и т.д.
-
Группировка кода
- Связанный код должен быть размещён вместе.
- Зависимые функции должны быть близко, для облегчения понимания их взаимодействия.
- Похожие функции должны быть близко.
-
Используйте пустые строки для разделения логической структуры, разделения слабо связанных частей.
// ❌ Плохо 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 } -
Держите строки короткими:
Короткие строки облегчают чтение кода и предотвращают горизонтальный скроллинг.
// ❌ 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"; -
Не используйте горизонтальное выравнивание и не нарушайте отступы:
Горизонтальное выравнивание переменных и комментариев затрудняет внесение изменений и поддержание кода. Когда вы выравниваете переменные и комментарии по горизонтали, изменение одного элемента может потребовать перестановки остальных элементов на той же линии. И
rustfmtвсё равно его уберёт, а выравнивание вручную усложняет правки.// ❌ let first_variable = 1; // Первый комментарий let second_variable = 2; // Второй комментарий // ✅ let first_variable = 1; // Первый комментарий let second_variable = 2; // Второй комментарий
Объекты и структуры данных
Объекты и структуры данных
- Упаковывайте внутреннее устройство в безопасный обьект
Приватность особенностей внутренней структуры обьекта, помогает избежать зависимостей от конкретных деталей реализации и делает ваш код более устойчивым к будущим изменениям.
// ❌ публичные поля структуры, позволяют использовать ее неверным способом 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(); } }
- Предпочитайте структуры данных
Предпочитайте простые структуры данных без избыточного состояния и кэшей, чтобы уменьшить сложность и повысить ясность кода. Логика/поведение должна быть отделена от хранения данных, чтобы облегчить сопровождение и повысить читаемость кода. Избегайте дублирования данных и логики внутри структуры, поддерживайте единую точку истины. Гибридные структуры, которые смешивают поведение и данные, могут быть трудны для понимания и сопровождения. Стремитесь к тому, чтобы структура данных и объекты имели ясное разделение.
✅ структура хранит данные, методы просто оперируют с этими данными (чистые вычисления или простые изменения).
❌ структура хранит данные и дублирует вычисления и состояние.
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() } }
- Объекты должны делать одно дело
Объекты и структуры данных должны быть сфокусированы на выполнении одной задачи.
- Базовый класс не должен знать о своих производных
В объектно-ориентированном программировании, базовый класс не должен иметь зависимости от своих производных классов.
В Rust вместо базовых классов и наследников используют трейты для описания поведения. Трейт не знает ничего о типах, которые его реализуют — это уже реализаторы (типы) знают про трейт, а не наоборот. Поэтому в Rust эта проблема (базовый класс, знающий о производных) просто не возникает.
Однако, есть случаи, которые можно рассматривать как «нарушение духа» этого принципа, например:
-
- Зависимость от конкретных типов в трейте через ассоциированные типы
Если в трейте есть ассоциированные типы (associated types) или связанные константы, и мы заставляем реализацию использовать определённые типы, то в некотором смысле трейт начинает иметь представление о типах, связанных с реализациями.
pub trait Animal { type Food; fn eat(&self, food: Self::Food); }
Но даже здесь трейт просто определяет интерфейс с «параметром», а не конкретно зависит от реализации.
-
- Типажи с вызовом конкретных реализаций (в редких случаях)
Если в трейте сделать метод с дефолтной реализацией, который зависит от конкретного типа через downcasting или unsafe — это плохая практика и похожа на нарушение принципа. Впрочем, в стандартном Rust таких вещей не делают.
-
- Связь через замыкания, где трейт «знает» о конкретных типах через параметры
Иногда, когда трейт имеет метод, принимающий конкретный тип или замыкание с конкретным захватом, это создаёт более сильную связь, но опять же это касается конкретных вызовов, а не самого трейта.
- Лучше иметь много функций, чем передавать код в функцию для выбора поведения
Разделение кода на множество функций, позволяет лучше организовать логику и избежать сложных конструкций, таких как флаговые аргументы.
// ❌ использование флага для выбора поведения 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) { // Без скидки }
- Предпочитайте нестатические методы статическим
Нестатические методы (&self/&mut self) позволяют работать с состоянием экземпляра, обеспечивают полиморфизм и расширяемость, в то время как статические методы не имеют доступа к состоянию и чаще используются как конструкторы или вспомогательные функции. В Rust это не строгий догмат, а рекомендация, чтобы делать код более идиоматичным и понятным.
Code smells
Code smells
Code smells — это признаки того, что код может содержать потенциальные проблемы или области для улучшения. Увеличивают когнитивную нагрузку на программиста.
- 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) } }
- 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(); } }
- 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) } }
- 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 } }
- 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() }
- 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 по Фаулеру
Этот список — не догма, а скорее набор ориентиров, которые помогают находить слабые места в коде и понимать, какие рефакторинги можно применить для их исправления.
Признаки, связанные с методами (функциями)
-
Длинный метод (Long Method)
- Что это: Метод, который слишком длинный и делает слишком много вещей.
- Почему это плохо: Снижает читаемость, усложняет понимание и повторное использование.
-
Большой класс (Large Class)
- Что это: Класс, который пытается делать слишком много и содержит множество полей, методов и ответственностей.
- Почему это плохо: Нарушает принцип единственной ответственности (SRP), становится "богом-объектом".
-
Цепочка вызовов (Message Chains)
- Что это: Длинные последовательности вызовов вида
a.getB().getC().getD().doSomething(). - Почему это плохо: Создает сильную связь между клиентом и структурой объектов. Изменение в одном звене цепи сломает весь код.
- Что это: Длинные последовательности вызовов вида
-
Завышенные права (Feature Envy)
- Что это: Метод одного класса чрезмерно часто использует данные и методы другого класса.
- Почему это плохо: Показывает, что метод, скорее всего, должен находиться в том классе, к данным которого он так часто обращается.
-
Интимные отношения (Inappropriate Intimacy)
- Что это: Классы слишком много знают о внутреннем устройстве друг друга (часто через доступ к приватным полям/методам).
- Почему это плохо: Сильная связность, изменение одного класса почти всегда ведет к изменению другого.
-
Расходящиеся модификации (Divergent Change)
- Что это: Когда один класс приходится часто менять по разным причинам (например, при изменении в БД и при изменении в формате отчета).
- Почему это плохо: Нарушение SRP. Класс должен меняться только по одной причине.
-
Стрельба дробью (Shotgun Surgery)
- Что это: Противоположность "Расходящимся модификациям". Одно изменение в системе требует внесения множества мелких правок в множество классов.
- Почему это плохо: Сложно отследить все места для изменений, легко что-то упустить.
-
Парализующий страх перед добавлением метода (Parallel Inheritance Hierarchies)
- Что это: Частный случай "Стрельбы дробью". При добавлении подкласса в одну иерархию, вам приходится добавлять подкласс и в другую, связанную с ней иерархию.
Признаки, связанные с данными
-
Длинный список параметров (Long Parameter List)
- Что это: Метод с большим количеством параметров.
- Почему это плохо: Трудно понять и использовать, легко перепутать порядок аргументов.
-
Группа данных (Data Clumps)
- Что это: Несколько полей (например,
String country,String city,String street) или параметров, которые всегда передаются вместе. - Почему это плохо: Лучше объединить их в отдельный класс-структуру (например,
Address).
- Что это: Несколько полей (например,
-
Примитивная мания (Primitive Obsession)
- Что это: Использование примитивных типов (строк, чисел) вместо маленьких объектов для представления доменных понятий (например,
String phoneвместо классаPhoneNumber). - Почему это плохо: Упускается возможность поместить поведение, связанное с этими данными, в один класс.
- Что это: Использование примитивных типов (строк, чисел) вместо маленьких объектов для представления доменных понятий (например,
-
Отклоняющийся код (Speculative Generality)
- Что это: Наличие классов, методов или полей, которые "на всякий случай" добавлены для будущей функциональности, которая еще не нужна (YAGNI - You Ain't Gonna Need It).
- Почему это плохо: Бесполезно усложняет код.
Признаки, связанные с наследованием
-
Отказ от наследства (Refused Bequest)
- Что это: Подкласс использует только часть унаследованных методов и свойств родителя.
- Почему это плохо: Нарушает принцип подстановки Барбары Лисков (LSP). Часто лучше использовать композицию вместо наследования.
-
Дублирование кода (Duplicate Code)
- Что это: Один и тот же или очень похожий код встречается в нескольких местах.
- Почему это плохо: Главный враг сопровождения. Исправление ошибки требует изменений в нескольких местах.
-
Временное поле (Temporary Field)
- Что это: Поле в классе, которое заполняется и используется только в определенных scenarios (часто в одном методе), а в остальное время равно
nullили не имеет смысла. - Почему это плохо: Сбивает с толку, так как ожидается, что поля объекта всегда имеют осмысленные значения.
- Что это: Поле в классе, которое заполняется и используется только в определенных scenarios (часто в одном методе), а в остальное время равно
Общие признаки и запахи дизайна
-
Комментарии (Comments)
- Важно: Фаулер не против комментариев вообще. "Запахом" являются комментарии, которые маскируют плохой код. Часто лучше отрефакторить код, чтобы он стал самодокументируемым, чем писать поясняющий комментарий.
-
Ленивый класс (Lazy Class)
- Что это: Класс, который слишком мал и не несет достаточной пользы, чтобы оправдать свое существование (например, после рефакторинга он стал почти пустым).
- Почему это плохо: Бесполезная сложность.
-
Данные-классы (Data Class)
- Что это: Класс, который содержит только поля и геттеры/сеттеры, без какой-либо бизнес-логики.
- Почями это плохо: Поведение, связанное с этими данными, размазано по другим классам, что приводит к "Завышенным правам".
-
Свистопляска с операторами switch (Switch Statements)
- Что это: Длинные или повторяющиеся операторы
switch(илиif/else if). - Почему это плохо: Часто нарушают OCP (Принцип открытости/закрытости). Добавление нового case требует модификации существующего кода. Решение — полиморфизм.
- Что это: Длинные или повторяющиеся операторы
-
Игнорирование исключений (Ignored Exceptions)
- Что это: Пустой блок
catch. - Почему это плохо: Ошибка подавляется, программа продолжает работать в неконсистентном состоянии, что приводит к непредсказуемым последствиям.
- Что это: Пустой блок
-
Альтернативные классы с разными интерфейсами (Alternative Classes with Different Interfaces)
- Что это: Два класса выполняют схожую функцию, но имеют разные имена методов или сигнатуры.
- Почему это плохо: Запутывает и усложняет выбор правильного класса.
-
Недостоверные имена (Mysterious Name)
- Что это: Имена переменных, методов или классов, которые неясно отражают их назначение.
- Почему это плохо: Код становится нечитаемым. Хорошее именование — одна из самых важных вещей в программировании.
-
Минимизация повторений (DRY - Don't Repeat Yourself):
Избегайте дублирования кода. Если у вас есть повторяющийся код, вынесите его в отдельную функцию, метод или класс.
-
Маленькие функции (Small Functions):
Функции должны быть небольшими и выполнять одну четко определенную задачу. Если функция становится слишком большой, разделите ее на более мелкие функции с понятными именами.
-
Принцип единственной ответственности (Single Responsibility Principle - SRP):
Каждый класс или функция должны быть ответственными только за одну вещь. Это облегчает понимание и изменение кода.
-
Принцип открытости/закрытости (Open/Closed Principle - OCP):
Код должен быть открыт для расширения, но закрыт для модификации. Это достигается путем использования абстракций и полиморфизма.
-
Тестирование:
Пишите тесты для вашего кода, чтобы обеспечить его корректность и устойчивость к изменениям. Следуйте принципу "Тестирование приводит к чистому коду". Тесты заставляют думать о дизайне — чтобы код было легко тестировать, он обычно становится более модульным, с хорошо отделёнными зонами ответственности.
- Раннее обнаружение ошибок — тесты выявляют баги сразу, предотвращая накопление сложных проблем.
- Документация поведения — тесты служат живым описанием того, как должен работать код.
- Упрощение рефакторинга — с тестами можно смело менять структуру кода, не боясь сломать функциональность.
- Меньше избыточной сложности и дублирования — чтобы тесты были простыми, код обычно становится проще и чище.
-
Если нашли место для рефакторинга, следует это рефакторить:
При внесении изменений в код, следите за тем, чтобы код оставался чистым или становился чище, чем был до ваших изменений.
Правило бойскаута - "Оставьте место стоянки чище, чем оно было до вас". Правило бойскаута помогает избежать накопления технического долга и поддерживать кодовую базу в хорошем состоянии без лишних усилий.