Рефакторинг legacy кода на Rust: пошаговое руководство

Онлайн-тренажер Python для начинающих

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

Начать курс

Введение: почему рефакторинг legacy кода на Rust — это вызов и необходимость

Работа с устаревшим (legacy) кодом — неизбежная часть жизни любого разработчика. В экосистеме Rust, известной своей строгостью и безопасностью, рефакторинг legacy кода приобретает особую специфику. С одной стороны, компилятор Rust — ваш лучший друг, который ловит множество ошибок на этапе компиляции. С другой стороны, наследие может включать unsafe-блоки, запутанную логику владения или устаревшие идиомы, которые не соответствуют современным практикам.

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

Шаг 1: Анализ и изоляция — прежде чем трогать код

Первый и самый важный этап — понять, с чем вы имеете дело. Не пытайтесь рефакторить всё сразу. Используйте стратегию "бойскаутского правила": оставьте код чище, чем он был до вас.

Используйте cargo и инструменты статического анализа

Запустите cargo check и cargo clippy. Clippy — это линтер, который предложит множество улучшений, от стилистических до семантических. Например, он может указать на излишние клонирования или неиспользуемые переменные.

// Пример: до рефакторинга
fn process_data(data: &Vec<i32>) -> i32 {
    let mut sum = 0;
    for i in 0..data.len() {
        sum += data[i];
    }
    sum
}

// Clippy предложит использовать итераторы:
fn process_data(data: &[i32]) -> i32 {
    data.iter().sum()
}

Напишите тесты для текущего поведения

Прежде чем менять логику, покройте существующий код модульными тестами. Это ваша страховочная сетка. Используйте cargo test и, если нужно, #[cfg(test)] модули.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_process_data() {
        let data = vec![1, 2, 3];
        assert_eq!(process_data(&data), 6);
    }
}

Шаг 2: Замена unsafe-блоков на безопасные абстракции

Legacy код на Rust часто злоупотребляет unsafe для "оптимизации" или из-за непонимания системы владения. Ваша цель — минимизировать unsafe до абсолютного минимума.

Выделите unsafe в отдельные функции

Если unsafe-блок нельзя убрать сразу, изолируйте его в небольшую функцию с понятным именем и документацией (Safety-секция).

// Плохо: unsafe размазан по всему коду
fn legacy_unsafe(data: &mut [u8]) {
    unsafe {
        let ptr = data.as_mut_ptr();
        // ... много unsafe операций
    }
}

// Хорошо: изоляция
/// # Safety
/// `data` должна быть валидной и не должна пересекаться с другими изменяемыми ссылками.
unsafe fn copy_inner(data: &mut [u8], src: *const u8, len: usize) {
    std::ptr::copy_nonoverlapping(src, data.as_mut_ptr(), len);
}

fn safe_wrapper(data: &mut [u8], src: &[u8]) {
    assert!(data.len() >= src.len());
    unsafe { copy_inner(data, src.as_ptr(), src.len()) };
}

Шаг 3: Рефакторинг с использованием современных идиом Rust

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

От структур с публичными полями к инкапсуляции

Legacy код часто содержит структуры с pub полями, что нарушает инкапсуляцию. Замените их на геттеры/сеттеры или методы.

// Legacy
pub struct Config {
    pub timeout: u64,
    pub retries: u32,
}

// Рефакторинг
pub struct Config {
    timeout: u64,
    retries: u32,
}

impl Config {
    pub fn new(timeout: u64, retries: u32) -> Self {
        Self { timeout, retries }
    }
    pub fn timeout(&self) -> u64 { self.timeout }
    pub fn retries(&self) -> u32 { self.retries }
}

Используйте enum вместо магических чисел

Если код полон констант, таких как 0, 1, 2, замените их на перечисления (enum). Это сделает код самодокументируемым.

// Плохо
fn handle_status(code: u32) {
    if code == 0 { /* OK */ }
    else if code == 1 { /* Error */ }
}

// Хорошо
enum Status {
    Ok,
    Error,
}

fn handle_status(status: Status) {
    match status {
        Status::Ok => { /* OK */ },
        Status::Error => { /* Error */ },
    }
}

Шаг 4: Оптимизация производительности и рефакторинг владения

Rust славится производительностью, но legacy код может содержать неэффективные паттерны, такие как излишнее клонирование или использование String там, где нужен &str.

Замените &Vec<T> на &[T]

Это улучшает гибкость и производительность, так как срез — это просто указатель и длина.

// Медленно и негибко
fn sum_vec(v: &Vec<i32>) -> i32 { v.iter().sum() }

// Быстро и гибко
fn sum_slice(v: &[i32]) -> i32 { v.iter().sum() }
// Теперь можно передавать и Vec, и массив [i32; N], и другие срезы

Используйте Cow (Clone-on-Write) для избежания лишних копий

Если функция может как возвращать владение, так и ссылку, используйте Cow<str> или Cow<[T]>.

use std::borrow::Cow;

fn process_name(name: &str) -> Cow<'_, str> {
    if name.starts_with("Mr.") {
        // Возвращаем ссылку, без копирования
        Cow                

Блоги

Рекомендаций по книгам