Clean Code на Rust: принципы и примеры
Написание чистого кода — это не просто следование модным трендам, а фундаментальный навык, который отличает профессионала от любителя. В мире системного программирования, где Rust набирает всё большую популярность, умение писать понятный, поддерживаемый и безопасный код становится критически важным. В этой статье мы разберём основные принципы Clean Code применительно к Rust, подкрепив их практическими примерами.
1. Именование: говорите, что делает переменная
Правильное именование — основа читаемого кода. В Rust, где строгая типизация и владение (ownership) уже дают много информации, хорошие имена делают код самодокументируемым.
Плохой пример:
fn calc(x: &[i32]) -> i32 {
let mut r = 0;
for i in x {
r += i;
}
r
}
Хороший пример (Clean Code):
fn calculate_sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for number in numbers {
total += number;
}
total
}
Используйте глаголы для функций (calculate, fetch, validate) и существительные для переменных (total, user_name, config). Избегайте сокращений вроде tmp, res или val — они ничего не говорят о назначении.
2. Функции: маленькие и с одной ответственностью
Принцип единственной ответственности (Single Responsibility) применим не только к классам, но и к функциям. Каждая функция должна делать ровно одно дело и делать его хорошо.
Плохой пример (функция делает слишком много):
fn process_user_data(user: &User) -> Result<(), String> {
// Валидация
if user.name.is_empty() {
return Err("Name is empty".to_string());
}
// Логирование
println!("Processing user: {}", user.name);
// Сохранение в БД
save_to_database(user)?;
// Отправка email
send_welcome_email(user.email)?;
Ok(())
}
Хороший пример (разделение на маленькие функции):
fn process_user_data(user: &User) -> Result<(), AppError> {
validate_user(user)?;
log_user_processing(user);
save_user_to_db(user)?;
send_welcome_email(&user.email)?;
Ok(())
}
fn validate_user(user: &User) -> Result<(), AppError> {
if user.name.is_empty() {
return Err(AppError::Validation("Name is required".into()));
}
Ok(())
}
fn log_user_processing(user: &User) {
println!("Processing user: {}", user.name);
}
Обратите внимание: каждая функция теперь имеет чёткую ответственность, а код стал легче тестировать и изменять.
3. Обработка ошибок: используйте Result и свои типы ошибок
В Rust нет исключений, и это прекрасно. Clean Code на Rust означает явную работу с ошибками через типы Result<T, E> и Option<T>. Избегайте паники (unwrap(), expect()) в production-коде.
Плохой пример (паника при ошибке):
fn read_config(path: &str) -> Config {
let content = std::fs::read_to_string(path).unwrap();
serde_json::from_str(&content).unwrap()
}
Хороший пример (чистая обработка ошибок):
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Failed to read file: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to parse config: {0}")]
ParseError(#[from] serde_json::Error),
}
fn read_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)?;
let config: Config = serde_json::from_str(&content)?;
Ok(config)
}
Используйте крейт thiserror для создания своих типов ошибок — это делает код более выразительным и упрощает отладку.
4. Идиомы Rust: используйте возможности языка
Clean Code в Rust — это не просто перенос привычек из других языков, а использование идиоматических конструкций самого Rust.
- Используйте
matchвместо цепочекif-else— это делает код более безопасным и читаемым. - Применяйте итераторы вместо явных циклов — они лаконичнее и часто производительнее.
- Используйте
OptionиResultвместо магических чисел (например, -1 для ошибки).
Плохой пример (стиль C++ в Rust):
fn find_user(users: &[User], id: u32) -> Option<&User> {
for i in 0..users.len() {
if users[i].id == id {
return Some(&users[i]);
}
}
None
}
Хороший пример (идиоматичный Rust):
fn find_user(users: &[User], id: u32) -> Option<&User> {
users.iter().find(|user| user.id == id)
}
Ещё один важный приём — использование строителей (Builder pattern) для сложных структур данных. Это улучшает читаемость и позволяет избежать длинных списков параметров в конструкторах.
#[derive(Debug)]
pub struct Query {
table: String,
condition: Option<String>,
limit: Option<u32>,
}
pub struct QueryBuilder {
query: Query,
}
impl QueryBuilder {
pub fn new(table: &str) -> Self {
Self {
query: Query {
table: table.to_string(),
condition: None,
limit: None,
},
}
}
pub fn with_condition(mut self, condition: &str) -> Self {
self.query.condition = Some(condition.to_string());
self
}
pub fn with_limit(mut self, limit: u32) -> Self {
self.query.limit = Some(limit);
self
}
pub fn build(self) -> Query {
self.query