5.13. Рекомендации по разработке на Rust
Рекомендации по разработке на Rust
Введение в культуру кода Rust
Rust формирует особую культуру разработки, основанную на строгой системе типов, гарантиях безопасности памяти без сборщика мусора и нулевых издержках абстракций. Эти принципы влияют на каждый аспект написания кода — от именования элементов до архитектурных решений. Культура кода в экосистеме Rust развивалась сообществом через годы обсуждений в репозиториях, на форумах и в официальных руководствах. Следование этим практикам упрощает чтение чужого кода, снижает количество ошибок и повышает производительность программ.
Ключевые ценности идиоматичного Rust кода включают явное управление владением ресурсами, предпочтение компиляторной проверки времени выполнения, использование трейтов для полиморфизма вместо наследования и обработку ошибок через типы Result и Option. Эти подходы формируют мышление разработчика, ориентированное на предотвращение классических проблем системного программирования ещё на этапе компиляции.
Соглашения об именовании
Основные правила именования
Rust следует единому стилю именования, определённому в официальном руководстве по стилю кода (Rust API Guidelines). Каждый элемент языка использует строго определённую нотацию:
| Элемент языка | Нотация | Пример |
|---|---|---|
| Модули, крейты | snake_case | network_utils |
| Типы (структуры, перечисления) | PascalCase | HttpRequest, StatusCode |
| Трейты | PascalCase | Display, Iterator |
| Функции, методы | snake_case | process_data, to_string |
| Переменные, параметры | snake_case | buffer_size, max_retries |
| Константы | SCREAMING_SNAKE_CASE | MAX_BUFFER_SIZE |
| Статические переменные | SCREAMING_SNAKE_CASE | GLOBAL_CONFIG |
| Жизненные циклы | строчные буквы | 'a, 'static |
| Обобщённые параметры | PascalCase | T, Item, ErrorType |
Имена должны точно отражать назначение элемента. Для логических переменных и функций, возвращающих bool, используйте префиксы is_, has_, can_, should_. Например: is_valid, has_permission, can_connect. Избегайте избыточных префиксов и суффиксов, таких как flag_ или _bool.
Именование перечислений и вариантов
Перечисления именуются в PascalCase, варианты перечислений также используют PascalCase. Для перечислений, представляющих ошибки, применяется суффикс Error. Для перечислений состояний — суффикс State.
enum ConnectionError {
Timeout,
Refused,
InvalidCertificate,
}
enum ProcessingState {
Idle,
Running,
Completed,
}
Именование методов
Методы, изменяющие состояние объекта, именуются глаголами в повелительном наклонении: push, pop, clear. Методы преобразования используют префикс to_ для неизменяемых преобразований и into_ для преобразований с потреблением исходного значения:
let s = String::from("hello");
let bytes = s.into_bytes(); // s больше не существует
let s2 = String::from("world");
let chars: Vec<char> = s2.chars().collect(); // s2 остаётся доступным
Методы, возвращающие ссылку на внутреннее состояние, используют префикс as_: as_str, as_slice. Методы, возвращающие итератор, заканчиваются на _iter или _into_iter.
Форматирование кода
Использование rustfmt
Форматирование кода в экосистеме Rust стандартизировано через инструмент rustfmt. Этот инструмент применяет единые правила отступов, пробелов и разрывов строк. Рекомендуется настроить автоматическое форматирование при сохранении файла в редакторе или перед коммитом через хуки Git.
Конфигурация форматирования хранится в файле rustfmt.toml в корне проекта. Базовая конфигурация включает:
edition = "2021"
max_width = 100
hard_tabs = false
tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Max"
Отступы и пробелы
Используйте четыре пробела для каждого уровня вложенности. Не применяйте символы табуляции. После ключевых слов if, while, for, match ставится пробел перед открывающей скобкой. Операторы сравнения и арифметические операторы окружены пробелами с обеих сторон.
// Правильно
if x > 10 && y < 20 {
process_value(x + y);
}
// Неправильно
if(x>10&&y<20){
process_value(x+y);
}
Расположение фигурных скобок
Фигурные скобки размещаются на той же строке, что и управляющая конструкция. Исключение составляют объявления функций и типов — открывающая скобка размещается на следующей строке при многострочном объявлении.
// Для управляющих конструкций
if condition {
do_something();
}
// Для функций с длинной сигнатурой
fn process_large_dataset(
data: &[u8],
config: &ProcessingConfig,
callback: impl FnMut(&[u8]),
) -> Result<(), ProcessingError> {
// тело функции
}
Вертикальное выравнивание
Для улучшения читаемости многострочных выражений применяйте вертикальное выравнивание. При вызове функций с множеством аргументов каждый аргумент размещается на отдельной строке с выравниванием:
let result = calculate_metrics(
input_data,
processing_parameters,
output_format,
error_handling_strategy,
);
При цепочках методов точка размещается в начале строки перед вызовом метода:
let filtered = data
.iter()
.filter(|x| x.is_valid())
.map(|x| x.normalize())
.collect::<Vec<_>>();
Структура проекта и организация модулей
Иерархия модулей
Проекты на Rust организуются через систему модулей. Корневой модуль определяется в файле src/lib.rs для библиотек или src/main.rs для бинарных приложений. Для крупных проектов применяется многофайловая структура модулей:
src/
├── lib.rs // точка входа в библиотеку
├── network/
│ ├── mod.rs // объявление модуля network
│ ├── tcp.rs
│ ├── tls.rs
│ └── http.rs
├── storage/
│ ├── mod.rs
│ ├── file.rs
│ └── memory.rs
└── utils/
├── mod.rs
└── math.rs
Файл mod.rs в каждой директории объявляет содержимое модуля и экспортирует публичные элементы:
// src/network/mod.rs
mod tcp;
mod tls;
mod http;
pub use tcp::TcpConnection;
pub use tls::TlsStream;
pub use http::{HttpRequest, HttpResponse};
Разделение интерфейса и реализации
Публичный интерфейс модуля определяется через ключевое слово pub. Внутренние детали реализации остаются приватными. Для экспорта элементов из вложенных модулей используйте цепочку pub use в родительских модулях. Это создаёт чистый и стабильный интерфейс для внешних пользователей крейта.
// src/lib.rs
pub mod network;
pub mod storage;
// Внешний код использует: my_crate::network::TcpConnection
Организация тестов
Тесты размещаются в модуле tests внутри файла или в отдельном файле tests/. Для unit-тестов применяется встроенный модуль #[cfg(test)]:
// src/network/tcp.rs
pub struct TcpConnection { /* ... */ }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connection_establishment() {
// тестовый код
}
}
Интеграционные тесты размещаются в директории tests/ на уровне проекта:
my_project/
├── src/
└── tests/
├── network_tests.rs
└── storage_tests.rs
Проектирование типов и трейтов
Принцип единственной ответственности
Каждый тип должен решать одну конкретную задачу. Структуры, объединяющие несвязанные данные, усложняют поддержку и тестирование. Разделяйте ответственности на отдельные типы с чёткими границами.
// Неправильно: структура с множеством несвязанных обязанностей
struct UserProcessor {
users: Vec<User>,
database: DatabaseConnection,
email_client: SmtpClient,
logger: Logger,
}
// Правильно: разделение ответственности
struct UserRepository {
connection: DatabaseConnection,
}
struct EmailService {
client: SmtpClient,
}
struct UserProcessor {
repository: UserRepository,
email_service: EmailService,
}
Использование новых типов
Паттерн новых типов (newtype pattern) повышает типобезопасность, оборачивая примитивные типы в структуры с семантическим значением:
struct UserId(u64);
struct EmailAddress(String);
fn send_notification(user_id: UserId, email: EmailAddress) {
// компилятор предотвращает передачу обычного u64 вместо UserId
}
Этот подход предотвращает ошибки смешения значений одинакового типа, но разного назначения.
Проектирование трейтов
Трейты определяют поведение, а не состояние. Избегайте трейтов с методами, изменяющими внутреннее состояние без чёткой семантики. Предпочитайте композицию трейтов наследованию.
Для расширяемости проектируйте трейты с минимальным набором обязательных методов. Остальные методы реализуйте как предоставленные (provided methods) с дефолтной реализацией:
trait Storage {
// Обязательный метод
fn save(&mut self, key: &str, value: &[u8]) -> Result<(), StorageError>;
// Предоставленный метод с дефолтной реализацией
fn load_string(&self, key: &str) -> Result<String, StorageError> {
let bytes = self.load(key)?;
String::from_utf8(bytes).map_err(|e| StorageError::InvalidData(e))
}
// Другой обязательный метод
fn load(&self, key: &str) -> Result<Vec<u8>, StorageError>;
}
Обобщённое программирование
Используйте обобщённые параметры для написания переиспользуемого кода. Ограничивайте обобщения трейтами вместо конкретных типов:
// Плохо: привязка к конкретному типу
fn process_data(data: Vec<i32>) { /* ... */ }
// Хорошо: обобщение с трейтом
fn process_data<T: Display + Clone>(data: Vec<T>) { /* ... */ }
Для сложных сценариев применяйте трейт-объекты через dyn Trait, когда обобщения не подходят из-за ограничений времени компиляции или необходимости динамической диспетчеризации.
Владение и заимствование в проектировании
Правила передачи владения
Функции принимают параметры по значению, когда им требуется владение ресурсом. Для чтения данных используйте заимствование через &T. Для изменения данных — через &mut T. Избегайте ненужного клонирования данных.
// Функция, требующая владения
fn take_ownership(value: String) {
// value уничтожается при выходе из функции
}
// Функция, заимствующая данные для чтения
fn read_only(value: &String) {
// value остаётся доступным после вызова
}
// Функция, заимствующая данные для изменения
fn modify(value: &mut String) {
value.push_str("!");
}
Возврат владения
Функции возвращают владение через тип возврата. Для возврата ссылки на внутреннее состояние применяйте аннотации жизненного цикла:
struct Container {
data: Vec<i32>,
}
impl Container {
// Возврат владения
fn extract_data(self) -> Vec<i32> {
self.data
}
// Возврат ссылки с жизненным циклом
fn get_data(&self) -> &Vec<i32> {
&self.data
}
}
Избегание лишних копий
Для типов, реализующих Copy, копирование происходит автоматически и бесплатно. Для остальных типов применяйте clone() осознанно. Предпочитайте заимствование перед клонированием:
// Плохо: ненужное клонирование
fn process_user(user: User) {
let name = user.name.clone();
println!("{}", name);
// user больше не доступен
}
// Хорошо: заимствование
fn process_user(user: &User) {
println!("{}", user.name);
// user остаётся доступным
}
Обработка ошибок
Использование типов Result и Option
Rust использует типы Result<T, E> и Option<T> для явного представления возможных ошибок и отсутствующих значений. Избегайте паник в обычном потоке выполнения. Паники допустимы только в ситуациях, которые представляют внутреннюю ошибку программы, которую невозможно корректно обработать.
// Плохо: паника при обработке пользовательского ввода
fn parse_number(input: &str) -> i32 {
input.parse().expect("Invalid number")
}
// Хорошо: возврат результата с ошибкой
fn parse_number(input: &str) -> Result<i32, ParseIntError> {
input.parse()
}
Создание кастомных типов ошибок
Для сложных приложений создавайте иерархию типов ошибок, реализующих трейт std::error::Error. Используйте библиотеку thiserror для упрощения создания ошибок:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ProcessingError {
#[error("Invalid input format: {0}")]
InvalidFormat(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Network timeout after {0} seconds")]
Timeout(u64),
}
Композиция ошибок через вопросительный оператор
Вместо вложенных блоков match используйте оператор ? для пробрасывания ошибок. Это упрощает чтение кода и делает поток управления линейным:
fn process_file(path: &Path) -> Result<(), ProcessingError> {
let content = fs::read_to_string(path)?;
let data = parse_content(&content)?;
validate_data(&data)?;
save_processed_data(&data)?;
Ok(())
}
Обработка цепочек ошибок
Для сохранения контекста ошибок применяйте методы context из библиотеки anyhow или создавайте обёртки вручную. Это помогает при диагностике проблем в продакшене:
use anyhow::{Context, Result};
fn load_config(path: &Path) -> Result<Config> {
let content = fs::read_to_string(path)
.context("Failed to read config file")?;
let config = serde_json::from_str(&content)
.context("Failed to parse config JSON")?;
Ok(config)
}
Асинхронное программирование
Выбор асинхронного рантайма
Rust поддерживает несколько асинхронных рантаймов: tokio, async-std, smol. Для большинства приложений рекомендуется tokio из-за зрелости экосистемы и производительности. Указывайте рантайм явно через атрибуты или макросы:
#[tokio::main]
async fn main() {
// асинхронный код
}
Структурирование асинхронного кода
Асинхронные функции помечаются ключевым словом async. Для ожидания завершения задачи используется .await. Избегайте блокирующих операций внутри асинхронных функций — используйте асинхронные альтернативы или выносите блокирующий код в отдельные потоки через tokio::task::spawn_blocking.
async fn fetch_data(url: &str) -> Result<Vec<u8>, reqwest::Error> {
let response = reqwest::get(url).await?;
response.bytes().await
}
Управление конкурентностью
Для параллельного выполнения задач используйте tokio::spawn или futures::future::join_all. Для ограничения параллелизма применяйте семафоры или пулы задач:
use tokio::sync::Semaphore;
use std::sync::Arc;
async fn process_batch(urls: Vec<String>) -> Vec<Result<Vec<u8>, reqwest::Error>> {
let semaphore = Arc::new(Semaphore::new(10));
let mut handles = Vec::new();
for url in urls {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let handle = tokio::spawn(async move {
let result = fetch_data(&url).await;
drop(permit); // освобождаем слот семафора
result
});
handles.push(handle);
}
futures::future::join_all(handles).await
}
Документирование кода
Структура документационных комментариев
Документация в Rust пишется в специальных комментариях с тройным слешем ///. Для модулей и крейтов используется комментарий //!. Документация компилируется в гипертекстовые страницы через cargo doc.
/// Представляет соединение с удалённым сервером.
///
/// Соединение устанавливается через метод `connect` и автоматически
/// закрывается при выходе из области видимости благодаря реализации трейта `Drop`.
///
/// # Примеры
///
/// ```
/// use my_crate::TcpConnection;
///
/// let mut conn = TcpConnection::connect("example.com:80")
/// .expect("Failed to connect");
/// conn.send(b"GET / HTTP/1.1\r\n\r\n").expect("Failed to send");
/// let response = conn.receive().expect("Failed to receive");
/// ```
pub struct TcpConnection {
// поля структуры
}
Элементы качественной документации
Каждый публичный элемент должен содержать:
- Краткое описание назначения
- Подробное объяснение поведения и ограничений
- Примеры использования в блоках кода
- Описание ошибок через секцию
# Ошибки - Описание паник через секцию
# Паники(если применимо) - Ссылки на связанные типы и трейты
Для методов описывайте взаимодействие параметров и возвращаемых значений. Указывайте предусловия и постусловия поведения.
Тестирование документации
Примеры кода в документации компилируются и выполняются как тесты при запуске cargo test. Это гарантирует актуальность примеров. Используйте атрибуты для управления выполнением:
/// # Примеры
///
/// Тестовый пример, который не должен выполняться:
/// ```no_run
/// let result = dangerous_operation();
/// ```
///
/// Пример для модуля:
/// ```ignore
/// // этот код не компилируется, но показывает концепцию
/// ```
Тестирование
Структура тестов
Разделяйте тесты на три категории:
- Юнит-тесты — тестируют отдельные функции и методы, размещаются в модуле
#[cfg(test)]внутри файла реализации - Интеграционные тесты — тестируют взаимодействие компонентов, размещаются в директории
tests/ - Документационные тесты — примеры в документации, проверяются через
cargo test --doc
Написание эффективных тестов
Каждый тест должен проверять одно конкретное поведение. Используйте описательные имена тестов в стиле snake_case, начинающиеся с test_:
#[test]
fn test_user_creation_with_valid_email() {
let user = User::new("test@example.com", "John");
assert!(user.is_valid());
}
#[test]
fn test_user_rejection_with_invalid_email() {
let result = User::new("invalid-email", "John");
assert!(result.is_err());
}
Для параметризованных тестов применяйте макросы или циклы внутри тестовой функции:
#[test]
fn test_email_validation() {
let valid_emails = vec![
"user@example.com",
"user.name+tag@example.co.uk",
];
for email in valid_emails {
assert!(is_valid_email(email));
}
}
Тестирование ошибок
Для проверки ожидаемых ошибок используйте методы unwrap_err или сопоставление с образцом:
#[test]
fn test_division_by_zero() {
let result = divide(10, 0);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), MathError::DivisionByZero);
}
Мокирование зависимостей
Для изолированного тестирования применяйте трейты вместо конкретных типов. Создавайте фиктивные реализации трейтов для тестов:
trait Database {
fn query(&self, sql: &str) -> Result<Vec<Row>, DbError>;
}
struct RealDatabase { /* ... */ }
impl Database for RealDatabase { /* ... */ }
#[cfg(test)]
struct MockDatabase {
responses: HashMap<String, Result<Vec<Row>, DbError>>,
}
#[cfg(test)]
impl Database for MockDatabase {
fn query(&self, sql: &str) -> Result<Vec<Row>, DbError> {
self.responses.get(sql).cloned().unwrap_or_else(||
Err(DbError::QueryFailed)
)
}
}
Безопасность и надёжность
Проверка безопасности памяти
Компилятор Rust гарантирует отсутствие использования после освобождения, двойного освобождения и гонок данных на этапе компиляции. Эти гарантии действуют без использования сборщика мусора. Для работы с небезопасным кодом применяйте блоки unsafe только при необходимости и сопровождайте их подробной документацией инвариантов.
Валидация входных данных
Все данные из внешних источников должны проходить валидацию перед использованием. Это включает:
- Проверку границ при работе с индексами
- Валидацию форматов строк (email, URL, даты)
- Ограничение размеров буферов для предотвращения переполнения
- Санитизацию пользовательского ввода перед использованием в системных вызовах
fn process_user_input(input: &str) -> Result<(), ValidationError> {
if input.len() > MAX_INPUT_LENGTH {
return Err(ValidationError::TooLong);
}
if !input.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(ValidationError::InvalidCharacters);
}
Ok(())
}
Защита от атак
При работе с сетевыми соединениями и внешними данными применяйте:
- Таймауты для всех операций ввода-вывода
- Ограничение скорости запросов
- Валидацию сертификатов TLS
- Использование параметризованных запросов для предотвращения SQL-инъекций
- Экранирование выходных данных для предотвращения XSS
Производительность
Измерение производительности
Для измерения производительности используйте бенчмарки через #[bench] или библиотеку criterion. Измеряйте реальные сценарии использования, а не микро-оптимизации.
use criterion::{criterion_group, criterion_main, Criterion};
fn benchmark_processing(c: &mut Criterion) {
let data = generate_test_data();
c.bench_function("process_large_dataset", |b| {
b.iter(|| process_data(&data))
});
}
criterion_group!(benches, benchmark_processing);
criterion_main!(benches);
Оптимизация через профилирование
Применяйте профилировщики (perf на Linux, Instruments на macOS) для выявления узких мест. Оптимизируйте только те участки кода, которые действительно влияют на производительность. Избегайте преждевременной оптимизации.
Выбор эффективных структур данных
Для разных сценариев используйте подходящие структуры:
Vecдля последовательного доступа и добавления в конецHashMapдля быстрого поиска по ключуBTreeMapдля упорядоченных данных и диапазонных запросовSmallVecилиArrayVecдля маленьких коллекций для избежания аллокацийCowдля избежания копирования при неизменяемом использовании
use smallvec::SmallVec;
// Для небольших списков до 8 элементов аллокация не происходит
let mut items: SmallVec<[Item; 8]> = SmallVec::new();
Избегание ненужных аллокаций
Переиспользуйте буферы вместо создания новых на каждой итерации. Применяйте методы, работающие на месте (sort вместо sorted), когда это возможно. Используйте итераторы вместо промежуточных коллекций.
// Плохо: создание промежуточных векторов
let result: Vec<_> = data
.iter()
.map(|x| x * 2)
.filter(|x| x % 3 == 0)
.collect();
// Хорошо: цепочка итераторов без промежуточных аллокаций
let sum: i32 = data
.iter()
.map(|x| x * 2)
.filter(|x| x % 3 == 0)
.sum();
Инструменты и конфигурация проекта
Конфигурация Cargo.toml
Файл Cargo.toml определяет метаданные крейта, зависимости и настройки сборки. Структурируйте зависимости по секциям [dependencies], [dev-dependencies] и [build-dependencies]. Указывайте версии зависимостей с использованием семантического версионирования.
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
authors = ["Developer <dev@example.com>"]
description = "Краткое описание проекта"
license = "MIT OR Apache-2.0"
repository = "https://github.com/user/my_project"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
[dev-dependencies]
criterion = "0.4"
mockall = "0.11"
[[bench]]
name = "processing_bench"
harness = false
Интеграция с clippy
Clippy — линтер для поиска распространённых ошибок и неидиоматичного кода. Запускайте его регулярно через cargo clippy. Настройте допустимые предупреждения в файле clippy.toml:
# clippy.toml
too-many-arguments-threshold = 8
Для подавления конкретных предупреждений используйте атрибуты:
#[allow(clippy::needless_return)]
fn example() -> i32 {
return 42; // clippy предлагает убрать return
}
Настройка среды разработки
Рекомендуемые инструменты для разработки на Rust:
- rust-analyzer — языковой сервер для автодополнения и навигации
- cargo-watch — автоматическая пересборка при изменении файлов
- cargo-expand — просмотр раскрытых макросов
- cargo-audit — проверка уязвимостей в зависимостях
- cargo-outdated — поиск устаревших зависимостей
Настройте редактор для автоматического форматирования через rustfmt при сохранении файла и запуска cargo check в фоновом режиме.
Практические паттерны проектирования
Построитель (Builder)
Паттерн построителя упрощает создание сложных объектов с множеством опциональных параметров:
pub struct HttpRequest {
method: Method,
path: String,
headers: HashMap<String, String>,
body: Option<Vec<u8>>,
}
pub struct RequestBuilder {
method: Method,
path: String,
headers: HashMap<String, String>,
body: Option<Vec<u8>>,
}
impl RequestBuilder {
pub fn new(method: Method, path: String) -> Self {
Self {
method,
path,
headers: HashMap::new(),
body: None,
}
}
pub fn header(mut self, name: &str, value: &str) -> Self {
self.headers.insert(name.to_string(), value.to_string());
self
}
pub fn body(mut self, body: Vec<u8>) -> Self {
self.body = Some(body);
self
}
pub fn build(self) -> HttpRequest {
HttpRequest {
method: self.method,
path: self.path,
headers: self.headers,
body: self.body,
}
}
}
// Использование
let request = RequestBuilder::new(Method::POST, "/api/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token")
.body(json_data)
.build();
Обёртка (Wrapper)
Паттерн обёртки добавляет функциональность к существующему типу без изменения его исходного кода:
pub struct LoggingWriter<W: Write> {
inner: W,
label: String,
}
impl<W: Write> LoggingWriter<W> {
pub fn new(inner: W, label: &str) -> Self {
Self {
inner,
label: label.to_string(),
}
}
}
impl<W: Write> Write for LoggingWriter<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
println!("[{}] Writing {} bytes", self.label, buf.len());
self.inner.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
Состояние (State)
Паттерн состояния инкапсулирует поведение, зависящее от текущего состояния объекта:
trait State {
fn handle(&self, context: &mut Context) -> Result<(), Error>;
}
struct DraftState;
struct PublishedState;
impl State for DraftState {
fn handle(&self, context: &mut Context) -> Result<(), Error> {
// логика для черновика
context.state = Box::new(PublishedState);
Ok(())
}
}
struct Context {
state: Box<dyn State>,
}
impl Context {
fn request(&mut self) -> Result<(), Error> {
self.state.handle(self)
}
}
Эти паттерны помогают создавать гибкие и поддерживаемые архитектуры, соответствующие принципам языка Rust.