ООП (объектно ориентированное программирование) против
ФП (функциональное программирование). Никогда бы раньше не подумал, что буду писать на эту тему. Казалось, что программист сам выбирает какую парадигму использовать и дело только в предпочтениях. Я сейчас не говорю о том, что из них лучше или хуже. Да, есть отличия, наверное, есть задачи, более подходящие для одного или другого. Но, как оказалось, каждый из этих подходов требует поддержку на уровне языка, причем не просто синтаксически. И да, языки делятся на ООП и ФП, но почему бы не использовать оба подхода в каждом из них, разве так нельзя? Можно, но лучше не надо! Основная проблема в том, что старые ЯП пытаются воссоздать конструкции из новых, не имея под капотом поддержки этих инструментов на базовом уровне, получается не очень. И наоборот, в современных ЯП воспроизводить старые решения не всегда возможно или оптимально. Рассмотрим несколько примеров в Rust.
Сначала давайте посмотрим, что у нас
под капотом. Мы уже много говорили про владение ресурсами и знаем, что по правилам, которые нельзя нарушать, на один ресурс нам разрешается иметь либо одну модифицирующую ссылку, либо несколько не модифицирующих. Также мы можем передать владение с помощью перемещения (оптимально) или копирования. При перемещении старый владелец удаляется, что не удобно, а при копировании, данные дублируются, что не эффективно. В си++ таких ограничений нет, хочешь несколько модифицирующих указателей на ресурс, пожалуйста, удобно, но не безопасно, один из них может удалить ресурс и второй про это никак не узнает. Нам конечно могут помочь умные указатели, но они не обязательное правило, что делает нарушение этого правила возможным. Тоже самое про перемещение владения, в Rust, для пользовательских типов оно происходит по умолчанию, это значит, что, если в пользовательском типе есть ссылка на ресурс, она будет перемещена, что исключит наличие двух ссылок на один ресурс. Для того чтобы произвести копирование, придется переопределить интерфейс копирования, в котором можно будет создать копию ресурса. В си++ наоборот, по умолчанию создается две ссылки на ресурс, что не безопасно, и можно определить или не определять конструкторы и операторы копирования. Все по причине обратной совместимости. Но если нет жестких правил, нет гарантии безопасности.
Теперь про
решения. Объектный подход, а именно классы и наследование, позволяют выстраивать иерархию типов и получать доступ к родителям и потомкам. Одна из возможностей в си++ иметь указатель на себя, создает циклическую связь, что невозможно в Rust по правилам, которые описывают время жизни типов, при которых потомок не может жить дольше родителя или ссылка на ресурс не может жить дольше самого ресурса. Вызов модифицирующего метода из типа, блокирует возможность получения одновременного модифицирующего или не модифицирующего доступа к полям этого же типа. Это требует специфического управления ресурсом, при котором мы не можем получать к нему параллельный модифицирующий доступ, но зато можем получить
последовательный,
передавая ресурс по цепочке от одной функции к другой. Не удобно? Не знаю. Пока для меня это ново, но подобные ограничения требуют альтернативных подходов. И они есть. Они другие. Лучше или хуже не мне судить. Например,
возможность хранить значения в перечислениях, значительно упрощает иерархию объектов, которую придется выстраивать в си++. Удобный отбор по образцу с деструктуризацией данных, все это позволяет решать задачи без необходимости применять интерфейсы и наследование для полиморфизма. Возможность их использовать тоже есть, но не является основой. В качестве примера – кусочек
компонентной системы (entity component system, ecs).
//Component types
#[derive(Debug, Eq, PartialOrd, PartialEq, Hash, Clone)]
enum ComponentType {
Transform(TransformComp),
Sprite(SpriteComp),
}
trait ComponentTrait {
fn init(&self) { println!("Default init method fo component"); }
fn update(&mut self) { println!("Default update method fo component"); }
fn draw(&self) { println!("Default draw method fo component"); }
}
//Transform Component
#[derive(Debug, Eq, PartialOrd, PartialEq, Hash, Clone)]
struct TransformComp {
owner: Option<String>,
x: i32,
y: i32,
}
impl TransformComp {
pub fn new(_x: i32, _y: i32) -> Self {
TransformComp {
owner: Option::None,
x: _x,
y: _y,
}
}
pub fn set_owner(&mut self, _owner: String) {
self.owner = Option::Some(_owner);
}
pub fn get_owner(&self) -> String {
match self.owner.as_ref() {
Some(own) => format!("{:?}", own),
None => format!("_"),
}
}
}
impl ComponentTrait for TransformComp {
fn init(&self) {
//
println!("For {} Init transform component. x:{}, y:{}", self.get_owner(), self.x, self.y);
}
fn update(&mut self) {
self.x += 1;
self.y += 1;
println!("For {} Update transform component. x += 1, y += 1", self.get_owner());
}
fn draw(&self) {
//
println!("For {} Draw transform component. x:{}, y:{}", self.get_owner(), self.x, self.y);
}
}
//Sprite Component
#[derive(Debug, Eq, PartialOrd, PartialEq, Hash, Clone)]
struct SpriteComp {
owner: Option<String>,
texture: String,
}
impl SpriteComp {
pub fn new(_text: String) -> Self {
SpriteComp {
owner: Option::None,
texture: _text,
}
}
pub fn set_owner(&mut self, _owner: String) {
self.owner = Option::Some(_owner);
}
pub fn get_owner(&self) -> String {
match self.owner.as_ref() {
Some(own) => format!("{:?}", own),
None => format!("_"),
}
}
}
impl ComponentTrait for SpriteComp {
fn init(&self) {
//
println!("For {} Init sprite component. I am {}", self.get_owner(), self.texture);
}
fn update(&mut self) {
//
println!("For {} Update sprite component. I am {} of 80 level", self.get_owner(), self.texture);
}
//use default method
// fn draw(&self) {
// //
// println!("For {} Draw sprite component. I am great {}", self.get_owner(), self.texture);
// }
}
//Entity
struct Entity {
name: String,
components_vec_by_type: Vec<ComponentType>,
}
impl Entity {
pub fn new(_name: &str) -> Self {
Entity {
name: _name.to_string(),
components_vec_by_type: Vec::new(),
}
}
pub fn add_comp_to_vec_by_type(mut self, mut _cmp: ComponentType) -> Self {
match _cmp {
ComponentType::Transform(mut cmp) => {
cmp.set_owner(self.name.to_string());
self.components_vec_by_type.push(ComponentType::Transform(cmp));
}
ComponentType::Sprite(mut cmp) => {
cmp.set_owner(self.name.to_string());
self.components_vec_by_type.push(ComponentType::Sprite(cmp));
}
};
self
}
pub fn get_comps_by_type(&mut self) -> &mut Vec<ComponentType> {
&mut self.components_vec_by_type
}
pub fn get_comps_by_trait(&mut self) -> Vec<&mut dyn ComponentTrait> {
let mut components_vec_by_trait: Vec<&mut dyn ComponentTrait> = Vec::new();
for cmp_type in self.components_vec_by_type.iter_mut(){
match cmp_type {
ComponentType::Transform( cmp) => {
components_vec_by_trait.push( cmp);
}
ComponentType::Sprite( cmp) => {
components_vec_by_trait.push( cmp);
}
};
}
components_vec_by_trait
}
}
fn main() {
//New Components
let tc1 = ComponentType::Transform(TransformComp::new(0, 0));
let sc1 = ComponentType::Sprite(SpriteComp::new("Wizard".to_string()));
//Add to Entity
let mut ent1 = Entity::new("Bob")
.add_comp_to_vec_by_type(tc1)
.add_comp_to_vec_by_type(sc1);
//Access by Component Type (enum)
for cmp_type in ent1.get_comps_by_type().iter_mut() {
match cmp_type {
ComponentType::Transform(cmp) => {
println!("Owner: {:?}", cmp.owner);
cmp.init();
cmp.update();
cmp.draw();
}
ComponentType::Sprite(cmp) => {
println!("Owner: {:?}", cmp.owner);
cmp.init();
cmp.update();
cmp.draw();
}
}
}
//Access by Component Trait (interface)
println!("Entity: {:?}", ent1.name);
for cmp in ent1.get_comps_by_trait().iter_mut(){
cmp.init();
cmp.update();
cmp.draw();
}
//
}
Перегуд В.