Avoiding accidental OOP-like programming

As a Kotlin developer for years, I finally got around to trying Rust.
I am implementing a CLI task management application with a REPL,
which maintains an internal modifiable task list.
In the future this application may receive events from the outside which will also modify this task list.

The odd thing is that, as global state is complicated in Rust,
I observed myself going into an OOP-like pattern:
The function with the REPL holds all state,
and if I cannot find a reasonable extraction,
I just process everything inside that function,
including creating "local functions" via closures.

This is less than ideal,
because sooner or later plenty of code will need access to that task list,
and I definitely do not want all that code to end up inside one function!

Is this a bad project to practice Rust
or am I doing something wrong?
The code is here, if it matters:

Feel free to pull the collection/state into a struct/enum/whatever, and implement methods on it.

Objects and method syntax are not the bad or defining part of OOP. The bad parts of OOP are mostly related to inheritance, subtyping, mutable aliasing, strong accidental coupling between types (and/or values), and so forth.

Giving names to things, grouping operations by type, and encapsulating state behind privacy barriers is good, actually. Incidentally, it was also the original point of OOP, before the industry hijacked the term.

13 Likes

There is an over-generalization which many Rust advocates make which goes: OOP has elements which work poorly in Rust; therefore OOP is bad and as a Rust programmer you must not write OOP. But this is, in my opinion, misleading; "OOP" is a bundle of lots of different ideas (some of which are in fact core parts of Rust), and you should not throw out all of them just because some of them don't fit.

For your particular situation, go ahead and write yourself an application state struct. Then, here are some specific OOP-thinking things you might want to avoid while doing that:

  • Avoid the idea that absolutely everything must be accessed through methods; don't hesitate to make use of fields directly. This will help you avoid borrow conflicts, because the compiler can see when you're borrowing two different fields.
  • Consider having your big struct contain smaller structs, more often than you might otherwise. In Rust, defining more types and nesting them is nearly free (unlike GC-based languages where every new object is a new allocation). This means that when you do write impl SomePart { fn do_thing(&mut self) {..., the &mut is only borrowing SomePart exclusively, and not the whole application state exclusively.
  • In general, remember that not every function must be a method.

Basically all of this is just about avoiding borrow conflicts by making the divisions of what is mutated and what is not more visible to the compiler / type system.

25 Likes

Thank you both! I indeed totally missed that possibility...

My code now is refactored into structs with methods and reasonable to work with again :slight_smile: more structs to come soon: mostr/src/tasks.rs at main - janek/mostr - Forging FTT Forensic Discovery GmbH

1 Like