Если нужно работать с штучним объектом, вдобавок менять у него сразу много полей, то лучше SoA. Use case: RPG игра (тыкаешь в одного персонажа), БД (достаешь одну запись), редактор (меняешь один объект):
// Работа с ОДНИМ объектом
fnfeed_oldest_animal(animals: &mut[Animal]){letoldest=&mutanimals[0];// берем 1 животное
oldest.health+=10.0;// работаем со всеми полями сразу
oldest.hunger-=5.0;}
ECS: Data-Oriented Design
Сложно поддерживать код SoA. На помощь в Rust приходят ECS (Entity Component System). Примеры - Bevy ECS, Hecs, Legion. ECS абстрагирует работу с памятью. Вы пишете компоненты как маленькие структуры:
А затем пишете Системы (функции), которые запрашивают только то, что им нужно:
// Пример из Bevy ECS
fnphysics_system(mutquery: Query<(&mutPosition,&Velocity)>){for(mutpos,vel)inquery.iter_mut(){pos.x+=vel.dx;pos.y+=vel.dy;}}
ECS-фреймворк сам раскладывает компоненты в памяти в виде плотных массивов (почти как SoA, чаще всего используя паттерн Archetypes). Когда physics_system запрашивает данные, то бежит по памяти линейно, идеально утилизируя кэш.
Array of Structs (AoS)
Если надо сделать массовую операцию, то лучше AoS. Use cases: 3D-графика (двигаем 10,000 частиц), нейросети (матричные операции), аудиообработка (тысячи сэмплов):
// Обновляем всех сразу
fnupdate_all_positions(particles: &mutParticles){// Векторизация! +SIMD ускоряет +х10 скорости
foriin0..particles.positions.len(){particles.positions[i].x+=particles.velocities[i].x;}}
“Hybrid” Arrays (AoSoA)
А есть вариант менять объекты подмножествами. То есть по несколько штук, но не все. Тогда гибридный подход AoSoA. Use case: комп игры, 3D-графика (эмиттеры частиц):
// Группируем по 8 частиц в "пачку"
structParticlePack{x: [f32;8],// 8 позиций X
y: [f32;8],// 8 позиций Y
vx: [f32;8],// 8 скоростей X
}letmutgame: Vec<ParticlePack>;// массив пачек
Придание дополнительного смысла типу переменной. Например, строка String может быть использована для чего угодно, а нам нужно конкретизировать:
// Обычный String
letname=String::from("john");// Newtypes - каждый тип уникален
structUsername(String);structEmail(String);structPassword(String);letuser=Username(String::from("john"));// Не перепутать типы
letemail=Email(String::from("john@example.com"));// Они разные!
У данного паттерна НЕТ стоимости в скорости или ресурсах приложения.
Применение
Защита от смешивания типов:
structMeters(f32);structKilometers(f32);fnrace_distance(distance: Meters){println!("Race is {} meters!",distance.0);}letm=Meters(100.0);letkm=Kilometers(0.1);race_distance(m);// ✅ Работает!
race_distance(km);// ❌ ОШИБКА! Нельзя передать КМ туда, где ждут М
pubstructCreditCardNumber(String);// клиенты видят этот метод
implCreditCardNumber{// Отдаём только безопасные методы
pubfnlast_four(&self)-> String{self.0[self.0.len()-4..].to_string()}// Внутренняя валидация происходит тут:
pubfnnew(number: String)-> Result<Self,String>{ifnumber.len()==16{Ok(CreditCardNumber(number))}else{Err("Must be 16 digits".to_string())}}}// Нельзя извне обратиться к сырой строке!
letcard=CreditCardNumber::new("1234567890123456".to_string()).unwrap();println!("{}",card.last_four());// "3456" ✅
// println!("{}", card.0); ❌ ОШИБКА! Приватное поле!
Когда использовать паттерн Newtype
Когда есть два значения одного типа, но означают разные вещи (IDs, измерения);
Когда надо добавить методы к типу, которым мы не владеем (например, обернуть u32, чтобы добавить is_even());
Когда надо применить правила валидации.
Когда НЕ использовать паттерн Newtype
Когда действительно нужны все методы исходного типа (вместо этого используйте псевдоним типа: type Age = u8).
У каждого класса/модуля/функции должна быть только одна причина для изменения.
Пример из жизни: кофемашина. Если она варит кофе, сама себя чистит, сама заказывает зёрна через интернет, печатает чеки - то это плохо! Если изменится способ печати чеков - придётся менять кофемашину. Если поставщик зёрен поменяет API - снова менять кофемашину.
Правильно: кофемашина только варит кофе. Чисткой занимается отдельный сотрудник, заказом зёрен — менеджер, печатью чеков — кассовый аппарат.
Пример антипаттерна в коде: SmartDevice::print_state() и хранит температуру, и выводит её в консоль:
implSmartDevice{pubfnprint_state(&self){println!("Текущая температура: {:.1}°C",...);// привязка к stdout
}}
Если потом потребуется писать отчёты в файл, отправлять по сети, выводить на веб-страницу, логировать в JSON, то придётся менять сам доменный тип, и это плохо!
Правильно: SmartDevice только хранит данные и отвечает за свою бизнес-логику. А за вывод отвечает кто-то другой.
Антипаттерн без Display:
structPerson{name: String,age: u8,}implPerson{/// SRP нарушен: тип сам знает, как выводить в консоль
pubfnprint_info(&self){println!("Person: {} Age: {}",self.name,self.age);}}fnmain(){letperson=Person{name: "Alex".into(),age: 20,};person.print_info();// вывод только в консоль
}
Паттерн SRP с Display:
usestd::fmt;structPerson{name: String,age: u8,}implfmt::DisplayforPerson{fnfmt(&self,f: &mutfmt::Formatter<'_>)-> fmt::Result{write!(f,"{} {}",self.name,self.age)}}fnmain(){letperson=Person{name: "Alex".into(),age: 20,};println!("{}",person);// вывод в консоль
lettext=format!("{}",person);// вывод в строку
std::fs::write("out.txt",text).unwrap();// вывод в файл
}
Что такое f: &mut fmt::Formatter<'_>:
Содержание (self - то, что надо “напечатать”)
Лист бумаги (f - куда Вы это записываете)
fmt::Formatter - это как лист бумаги + ручка в одном флаконе. Он знает:
куда писать (в консоль, в строку, в файл)
какие настройки форматирования (ширина, выравнивание и т.д.)
Что происходит в println:
println! создаёт свой Formatter, который указывает на стандартный вывод
Вызывает person.fmt(&mut formatter)
Метод fmt пишет в этот formatter
println! добавляет перевод строки и отправляет в консоль