Rust best practices

  1. Is it quite right to think and design in the OOP paradigm when you are implementing your ideas with Rust?
  2. Is there any best practices to design program's architecture in bounds of Rust?
  3. Any design patterns?
  4. Is it good when you're trying to fit the language you are writing on to the paradigm and pattern you used before in the OOP languages?

What do you think?

Given that Rust has structs, methods and traits I guess a certain amount of OOP thinking is appropriate. In general, the way I have seen OOP with inheritance and such, C++ style, I'm not convinced it's a ever a good paradigm. Which seems to be an opinion growing in popularity in the programming world in recent years. Ask Bjarne Stroustrup or see: "OOP The Trillion Dollar Disaster": https://medium.com/better-programming/object-oriented-programming-the-trillion-dollar-disaster-92a4b666c7c7

As to your point 4, I write most of my Rust as if I'm writing C. Until such time as the compiler tells me I cannot.

I'm not sure the Rust community has developed an idea of "best practices" yet. Most of us are still groping around trying to find out what we have in this language, which is a somewhat different language to what most are used to. Whatever those practices are probably depends greatly on your application.

I notice there is a strong trend to try and do everything in a "functional programming" style. Which I struggle with still.

Some OOP patterns are okay, but Rust is not really an object oriented language, and some of the patterns cause a lot of trouble. For example, some people use the pattern where they make an interface for every type, often with a single implementor, and this is not a good idea in Rust.

6 Likes

Writing tests is best.

The ideal design (behind the interfere you like, ie a simple one, and fast code,) is keeping hierarchical. parent->child->parent does not work with regular references and if writing otherwise (Rc, Arc) you may lose the benefit of static checking.

I don't think this is a good idea in any language (something, something, premature abstraction).

Some stereotypical OOP principles are equally as applicable to Rust.

For example, most of SOLID is just good practice:

  • Single-responsibility principle - each component should do one thing. Not much more to add here.
  • Open–closed principle - normally this "open for extension, closed for modification" principle is done using inheritance and virtual methods, but it can be done using traits and generics (think "dependency injection") or maybe using the "decorator pattern" as implemented by the various iterator combinators
  • Liskov substitution principle - generics. I don't care what type you actually give me, just that it implements a particular trait
  • Interface segregation principle - thin interfaces = better. This is best practice in pretty much every language with the concept of interfaces (e.g. rust traits, C# interfaces, C structs with function pointers)
  • Dependency inversion principle - "depend upon abstractions, not concrete types". Makes sense to me. You see this all over the place in things like the embedded_hal and std (e.g. std::io::Write or when you accept T: Into<Foo>)

You can forget most of the Gang of Four patterns though. They're mostly applicable to the stereotypical OO language with a GC, they can be implemented in Rust, but things like the visitor pattern or abstract factories are better done using enums and functions, respectively.

You may find the API Guidelines useful. They aren't design patterns per-se (I define design patterns as constructs/strategies you can use to solve a particular problem), but there are some really good guidelines around how to write your code in a way that is idiomatic.

There's nothing wrong with that at all! I write C# as part of my day job and have noticed my Rust code tends to have a fairly OO leaning (e.g. I'll use traits and objects with attached behaviour instead of free functions or closures). Write however you like, as long as it fulfills its purpose and isn't a pile of :poop:, most people won't mind.

You just need to know when it's appropriate to use patterns from other languages, and when it isn't.

The compiler helps a lot here. Often when you're trying to force a square peg into a round hole it'll complain about lifetimes or force you to use janky workarounds (e.g. Rc<RefCell<T>> because you came from a GC language and are used to everything having references to everything else).

7 Likes

Maybe you can take a look at DOD or Data Oriented Design paradigm instead of OOP. I think, OOP never was a good idea and e.g. for games it is a performance killer (for example cache misses)

And I think it is never a good idea to fit a language into something it isn't made for. And I think Rust isn't really made for (real) OOP. It is better to learn the new language paradigm. That's the problem of C++: it tries to make everybody happy

4 Likes

A number of concepts of OOP are very much still with us in Rust. To name just a few:

  • data encapsulation
  • private methods
  • interfaces
  • even extending/specializing interfaces, which is a form of inheritance

The main problem with OOP in my opinion is that we have grown accustomed to playing with fire. You need access to some other object in C++? Pass in a pointer or reference to it.

When using Rust, the compiler just won't let you play with fire, and you will find out that a number of things become much harder to design. Shared mutability specifically is really discouraged.

Other specifics like concurrent and parallel processing become more restricted with shared mutability disallowed. When async methods (eg. you don't know for how long they will run) start borrowing &mut self, using a typical OOP design becomes really hard.

For concurrency, the actor model is popular. For gaming people are playing with ECS.

I also think we still have to discover the design patterns over time.

3 Likes

I'd differ slightly about

I see a maybe young, but already-starting-to-become-standard direction of thought in the community about design patterns. I think you're partially correct with this statement in the sense that our ideas about best practices are still not fully-formed, but in kernel form, they're there.

However, your next sentence rings absolutely true:

I think this is quite a good summary. OOP is brilliant for designing GUI apps. It's easy to model the problem with widget classes in a hierarchical structure. Not so much for designing games. There OOP gets in the way of performance and cache.

About your comment about functional style, I can sympathize. I also first saw anything remotely like fp when I came to look at this Rust thing everybody is talking about. And I also have to reiterate that it isn't always the best / indicated way to design your logic.

However, I did tackle the Rust fp parts with the mindset of "Let's try to solve this cute toy problem in an fp way after I made it work with imperative constructs." I don't first think if this problem will be best solved in an imperative or fp style, I just try both! This proved to be extremely effective at teaching me functional idioms in Rust. At first, I'd write the imperative function very quickly and then get stuck for ages on the fp translation. Today, I can write an fp implementation without first implementing a working imperative version.

2 Likes

There is an effort to gather Rust design patterns and anti-patterns in rust-unofficial/patterns. It is far from being complete, but gives some overview.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.