Advice for unlearning OOP thinking

Hey folks,

I’ve been programming OOP since I learned Java from the Sun Certified Java Programmer 6 book back in so long ago. Ever since I’ve learned a bunch of other languages, but regardless of which one it was (Go, JS, Rust, you name it), I always wrote it in an OOP-y style. Somehow that comes naturally to me, and when it’s not possible (like in functional languages, or Odin, for example), I’m kind of lost, because, well, no OOP.

Any suggestions for how to unlearn the thinking? Any resources to watch? I just started Handmade Hero (day 2 here I come), but not sure if that would help (and also I’ve never written a single line of C, so who knows how much sticks).

Thanks all!

5 Likes

Good question!

You could watch this → https://www.youtube.com/watch?v=jyol7J1nlp8

And the Blow’s video then.

And this one could also be watched → https://www.youtube.com/watch?v=GKYCA3UsmrU

Instead of coupling the data with the code (in OOP) think about the data structure first. That means: make the structs, and then procedures to transform it.

entity_make ::  proc (...params...) -> EntityStruct {
...stuff...
}
move_entity :: proc (what_entity, displacement)  {
...
}
4 Likes

I’ve recently seen this talk that I liked:
The Big 00Ps: Anatomy of a Thirty-five-year Mistake

The title is somewhat misleading as it mostly talks about what people were doing before OOP and what are the alternatives.

What I gathered is essentially that most solutions people come up with is thinking interms of systems instead of cramming everything into classes even when it doesn’t make sense. OOP is useful in specific instances where the problem can be naturally represented with classes (like simulating networking where C++ came from).

My explanation perhaps isn’t the best so you should really watch the video, Casey is really good explaining things clearly.

3 Likes

Dude same, I was coming from js and Java and was trying to rewrite my game in Odin to test it out and I was like “where’s my classes!?” Come to find out it’s functional (sort of). I am now trying to train my brain to think differently. Thankfully Odin itself isn’t a hard language so I can guarantee you that it will come easy to you. Just learn what sets them apart and the language flow and you’re good.

I found this video:

An intro into what functional programming is.

I found it kinda helpful just to get the gist of what functional does and how it differs from the state hungry OOP. And I’m no pro at Odin by any means I just found myself really liking how different the flow is from most oop’s I’ve used.

1 Like

Come to find out it’s functional (sort of)

Not really, I mean not at all. Functional programming is related to treating functions as first-class primitives, and using function as the primary driver for computation. Rather than describing the program as a sequence of steps (imperative), functional programs are described as a function that can be composed of other functions. For example, something you might see in a functional language are functions like map, filter and reduce to work with arrays and transform them directly, as opposed to writing loop statements.

It’s also why it’s important to know about other paradigms and learn to think in different ways about how to write your programs. OOP will tell you that you have to bundle up your state and expose methods to manage that state, but you could have instead used a different approach, for example simply get from data A to data B, without any state management. There are situations where it is important to manage your state and perform correctness checks that the state didn’t become invalid, and there are other situations where all you need is a collection of algorithms that transform data into other data. So don’t try to unlearn the OOP, but use it alongside other paradigms.

One roadblock you may be hitting is trying to make your program more abstract than it needs to be. Thinking you might want to extend this, or have a system that’s capable of handling anything. You might start piling on interfaces, additional logic that handles situation that can’t happen, but might happen, because you might extend that. Odin makes writing interfaces harder on purpose. In reality you don’t need them that often.

For me encapsulation (bundling state and procedures together in a form of classes with methods) is one of the most powerful ideas in OOP that is unfortunately being discarded. In situations where you need to control state, I’d use this OOP-y approach, and in fact core library has its own fare share of encapsulation. String builder and parsers being the canonical state-manager structs. I suggest you look at them for how to adapt you OOP thinking into Odin. There are interfaces like io.Writer or mem.Allocator that can also show you how interfaces are done.

The road may suck, but remember to embrace the suck, and to keep it simple, stupid.

3 Likes

Object-Oriented C →

https://www.codementor.io/@michaelsafyan/object-oriented-programming-in-c-du1081gw2

https://dmitryfrank.com/articles/oop_in_c

And a C book:

If you are adventurous, then the source code for Wolfenstein is a good read, and so is the source for Doom :wink:

W informative information

It took me about 7 years to unlearn the OOP thinking and the major road blocker for me was that I couldn’t think of an alternative. Putting data into a class and then have methods exclusively operating on the data, ignoring any wider context just felt so natural do to.

The first thing that helped me get out of that thinking a little bit is the talk Practical Optimizations by Jason Booth. It introduced me to the idea that I only need one instance of a class to control the behaviour of a whole range of “objects”. This already reduced the complexity of my programs because because it removed the necessity of an external place to loop over all of my objects. It also made it easier to have those objects interact with each other, because the class already knows all of them.

The second step I needed was Caseys talk “The big OOPs”. For me the most significant part of that talk was that he demonstrated 3 examples of an Entity Component System and how it flips the encapsulation. Having those explicit examples instead of just vague explanations have been very important for me to understand the pattern.

My current programming style looks a bit like this:

  1. I have a “Universe” struct that holds pointers to maps of the components my system uses. Something like this:
EntityID :: distinct i32

Universe :: struct {
    entities: [dynamic]EntityID,
    next_id: EntityID,
    transform: map[EntityID]Transform,
    movement: map[EntityID]Movement,
    physics: map[EntityID]Physics,
    display: map[EntityID]Display,
    // ...
}
  1. All my functions have a pointer to the Universe they work on, e.g.:
update_physics :: proc(u: ^Universe, dt: f32) {
    // calculate physics for each entity with Physics
}

  1. Finally, my main loop just calls the different systems:
u: Universe
dt: f32
for {
   get_user_input(&u)
   update_actions(&u, dt)
   update_npc_movement(&u, dt)
   update_physics(&u, dt)
   // ...

   draw(&u)
}

My OOP brain still thinks of this as a single big object where all functions are methods for that object, but they really are just functions, so I can group them together thematically in separate files and within each function it’s easy to pull out the data I need and work on that data.

So maybe that helps as well giving you another example of how you could structure your program in a non-OOP way that allows you to reuse some part of your OOP thinking.

4 Likes

There’s a nice intuitive aspect with classes: linking the data and the operations on that data.

You can still do that with Odin, especially if are thoughtful about defining some basic types, organised structs, and writing procedures which takes those types as input - that’s key, because once your user-defined types or structs become part of the procedure type signature, you get that organisation and clarity.

If you’re after strictness, you can use distinct types to keep, for example, ints meant for foo away from ints meant for bar.

Distinct types #

A distinct type allows for the creation of a new type with the same underlying semantics.

My_Int :: distinct int
#assert(My_Int != int)

Aggregate types (struct, enum, union) will always be distinct even when named.

Foo :: struct {}
#assert(Foo != struct{})

But you do have more flexibility and thus responsibility to structure your code a certain way so the data and the procedures that operate on it are grouped in a nice way. Of course I can’t say for certain what conceptual paths are most productive for anyone, but hopefully this helps someone.

2 Likes