Паттерны проектирования


#1

Буэнос диас, амигос!

Понемногу изучая Rust, я столкнулся с тем, что привычные приемы проектирования “не работают”. Конечно же я об ООП и его паттернах проектирования. Некоторые “коллеги по цеху” уже разочаровались по стопицот раз, переписывая С++ код на Rust.

В представленной выше ссылке, как по мне, интересна не сама статья как натянуть сову на глобус, а последующие обсуждения. Там промелькнуло хорошее высказывание “Нет ООП-задач. Есть один из подходов - это используя ООП. Как будто он один единственный” (не дословно).

Собственно вопросы к обсуждению

Приступая к проектированию проекта, большинство разработчиков, которые худо-бедно владеют ЯП с полной поддержкой ООП, этот подход и выбирают. Я не имею ввиду 100-строчные “хелоу ворлд” утилиты. А нормальные, более-менее сложные проекты.

  1. А как быть если полной поддержки ООП в ЯП нет?
  2. Есть ли приемы/методики проектирования не менее удобные чем объектно-ориентированные?
  3. А есть ли для них свои “паттерны проектирования”, которые есть для ООП?

:face_with_raised_eyebrow:

ЗЫ: Аббревиатуру “ООП” читаем как “Объектно-ориентированное программирование” или “Объектно-ориентированное проектирование” по контексту.


#2

Я так понимаю, что автору той статьи просто не хватило упорства поработать с третьим пунктом, то есть с типами вида Vec<Rc<RefCell>>. А парента делать с помощью WeakRef.

А конкретно по вопросам:

  1. А как быть если полной поддержки ООП в ЯП нет?

Ну как, пользоваться тем, что есть. Для чего больше всего используется ООП? Для возможности единообразно работать с объектами разных типов (утрирую, конечно). А для этой задачи есть интерфейсы, то есть типажи.

  1. Есть ли приемы/методики проектирования не менее удобные чем объектно-ориентированные?

Проблема в том, как я обычно говорю, что как бы ты не программировал на ООП языке, ты всегда вляпаешься в какой-нибудь паттерн, даже не зная его. Специальных Растовых методик, заменяющих всю историю ООП, я не встречал.

  1. А есть ли для них свои “паттерны проектирования”, которые есть для ООП?

Мне кажется, сюда можно отнести разные конструкции типа let-match, работа с итераторами (всякие filter, collect и т.п.) и правильный выбор умных указателей, о чём как раз говорилось в статье.


#3

Есть вот такая репа:

Но там много какие пункты это TODO.

Еще в Книге есть 17я глава “Object Oriented Programming Features of Rust”, там есть некоторые вводные мысли про ооп в расте.


#4

Раст это гибридный язык взявший что то от мира ООП что то от функциональщины. Но из за этого не все функциональные подходы ложатся в язык и не все ООП. Это совершенно новый путь в разработке и тут будут совсем другие паттерны проектирования. Конечно некоторые методики уже хорошо ложатся в раст, некоторые выглядят сложно и не красиво на расте, некоторые вообще не возможно сделать. Язык относительно молодой и еще не все подходы были открыты. Так же у людей кто его использует в работе просто нет времени поделится мыслями о новых способов программирования на расте. По этому многие рекомендуют читать исходники когда речь заходит “А подскажите паттерны или бест практикс”.

Советую посмотреть данный не плохой доклад. Частично затрагивается наша обсуждаемая тема с упором на паттерн Entity Component System.


#5

Если говорить о паттернах и ООП, то нельзя не упомянуть одно из последних нововведений - процедурные макросы. Они позволяют существенно облегчить боль от отсутствия механизма наследования в Rust.
В книге есть пример как сделать свой типаж “наследуемым”, т.е. просто добавив в определение структуры атрибут #[derive(HelloMacro)] мы получаем автоматическую реализацию типажа HelloMacro.
Можно ввести дополнительные атрибуты, которые позволят указывать какие поля структуры используются в реализации.

Например, пусть функция hello_macro() нашего типажа выводит поле name:

#[derive(HelloMacro)]
struct Pancakes{
    name:String,//field 'name' is displayed by default using HelloMacro
    address:u64
}

Для структуры Pancakes автоматически сгенерируется такой код:

impl HelloMacro for Pancakes {
    fn hello_macro(&self) {
        println!("Hello, {}!", self.name);
    }
}

Чтобы реализовать HelloMacro для структуры в которой нет поля name, добавим собственный атрибут #[name], который скажет нашему макросу какое поле использовать в реализации типажа:

#[derive(HelloMacro)]
struct Cookies{
    #[name]
    xxx:&'static str,
    yyy:u8
}

Автоматически сгенерированный код:

impl HelloMacro for Cookies{
    fn hello_macro(&self) {
        println!("Hello, {}!", self.xxx);
    }
}

Все можно сделать очень красиво и с выводом своих ошибок в случае неправильного использования атрибутов. Код процедурных макросов должен быть расположен в отдельном крейте. Его нужно указать в разделе [dependencies] в файле Cargo.toml.

Сложность в том, что все граничные случаи и возможности применения кастомных атрибутов приходится прописывать самому. Заниматься этим имеет смысл если вы хотите сделать максимально простой в использовании API для своей библиотеки.

Вот немного допиленный код примера из книги:

Крейт pancakes:
pancakes\src\main.rs

use hello_macro_derive::HelloMacro;

trait HelloMacro {
    fn hello_macro(&self);
}

#[derive(HelloMacro)]
struct Pancakes{
    name:String,//field 'name' is displayed by default using HelloMacro
    address:u64
}

//#[derive(HelloMacro)]//no field `name` on type `&Dumplings`
struct Dumplings{
    xxx:usize
}

#[derive(HelloMacro)]
struct Cookies{
    #[name]
    xxx:&'static str,
    //#[name]//Attribute #[name] applied to multiple fields
    yyy:u8
}

//#[derive(HelloMacro)]
struct  Ravioli(
    //#[name]//Attribute #[name] applied to unnamed field
    usize
);

//#[derive(HelloMacro)]//Attribute #[derive(HelloMacro)] applied to something that is not a struct
enum Rabbit{}

fn main() {
    Pancakes{ name:"World".to_owned(), address:0x80000001 }.hello_macro();
    Cookies{ xxx:"Maria", yyy:1 }.hello_macro();
}

pancakes\Cargo.toml

[package]
name = "pancakes"
version = "0.1.0"
edition = "2018"

[dependencies]
hello_macro_derive = { path = "../hello_macro_derive" }

Крейт hello_macro_derive:

hello_macro_derive\src\lib.rs

extern crate proc_macro;
extern crate proc_macro2;

use proc_macro::TokenStream;
use quote::quote;
use proc_macro2::{Ident, Span};

#[proc_macro_derive(HelloMacro, attributes(name))]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let ast = syn::parse(input).unwrap();
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let defid = Ident::new("name", Span::call_site());
    let name = &ast.ident;
    let mut fieldid = None;

    if let syn::Data::Struct(ref strct) = ast.data{
        for field in strct.fields.iter(){
            for attr in field.attrs.iter(){
                if attr.path.is_ident("name"){
                    if fieldid.is_some() { panic!("Attribute #[name] applied to multiple fields"); }
                    fieldid = Some(field.ident.as_ref().expect("Attribute #[name] applied to unnamed field"));                    
                }
            }
        }
    }else{
        panic!("Attribute #[derive(HelloMacro)] applied to something that is not a struct");
    }
    
    let fieldid = fieldid.unwrap_or(&defid);

    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro(&self) {
                println!("Hello, {}!", self.#fieldid);
            }
        }
    };
    gen.into()
}

hello_macro_derive\Cargo.toml

[package]
name = "hello_macro_derive"
version = "0.1.0"
edition = "2018"

[lib]
proc-macro=true

[dependencies]
syn = "*"
quote = "*"
proc-macro2 = "*"