Everybody who writes code, whether they’re self-taught or took classes in school, encounters the concepts of object-oriented programming. It’s in every book, it’s behind most APIs, and it’s been the focus of language designs for the past 20-25 years. In both academia and the industry, the conventional wisdom is that OOP is the best, maybe only, way to manage a large codebase. But that hasn’t been my experience, and it hasn’t been the experience of many people, and I think many coders, especially in game programming, are starting to look for alternative approaches to code organization. What I want to do here is go over a couple of aspects of OOP that I have personally found to be roadblocks to getting things done, and then look at some information for alternative approaches.
OOP vs Experience
The problems engendered by OOP vary from person to person, but in my experience there are two main tenets of OOP that keep causing problems when I use them for the construction and organization of code: 1) modelling after real-world objects , and 2) the insular functioning of objects. The first is problematic because aiming to create code that models real-world objects almost always leads to unnecessary abstractions that conceptually muddle the aims and functioning of the program. Our code doesn’t work with “real-world” objects. It works with data. Inputting and outputting data. Of course, wrapping data into abstractions is almost always beneficial at some point, e.g. when working with a
Color object is preferable to working with independent RGBA floats, but the abstractions should come about naturally from the needs of the code, rather than being designed up front from what we expect the code to be. I feel like starting a project with a bunch of UML diagrams ignores the fact that I don’t know what the final state of the code will be, even if I know what the program will do.
The second tenet that I find problematic probably has a better technical name, but what I mean by “insular functioning of objects” is that typical OOP takes the mindset of working on one object at a time, usually in a hierarchy. I’m thinking of a scene graph, for example: I take one entity and update it. Then I take each child entity, update those, and then follow their child entities, and so on, until I’ve covered the entire graph. This approach has some advantages, but I think that when I’ve followed this design, I’ve done so because that’s the abstraction that I was starting with, not because the data I was working with was hierarchical. In fact, what I usually find is that the code only needs to work with parts of entities at one time. For example, I need just the position and velocity to update position. Or I just need the time keeping and textures to do the animation. But the code almost never needs the entire entity with all its member data. This approach is closer to how we normally handle particle systems. And the big reveal here, at least for me, is that the easiest, most direct thing to do is almost always the particle system approach, while typical OOP would have us starting with a scene graph.
Component-Entity-System: The component-entity-system (CES) approach was my first step away from the traditional
SubSubClass inheritance model, and it has saved me many, many hours of frustration. Adding data & functionality through inheritance leads to tightly coupled systems, and this makes growing the code virtually impossible. CES, on the other hand, keeps entities as plain containers of components, adds data through components, and functionality through systems. Components are then be added and removed from entities as needed. Systems typically keep track of components and do the work of updating them. Plenty of people are writing about CES, but I found this to be a great introduction, this to be really useful in getting into more details.
Compression Driven Programming: Compression driven programming is also known as semantic compression, and it is founded in the idea that structures in your code should emerge from their usage, as opposed to being designed up front. The concept of compression here is the same as GZIP compression. Start by writing your code to do what needs to be done, without functions or data structures. When you find that some code gets used more than two or three times, then that’s a candidate for “compression”. Pull it out and make it a function. Likewise, when you find that some data always gets passed around together (like the RGBA floats of a color), then that’s also a good candidate for “compression”. Pull it out and make a
class. Following this approach ensures that you only have the abstractions that make sense in your code. The article that everyone references is this one by Casey Muratori. I think this commentary on that article is good for pointing out that there are good and bad ways to compress and come up with abstractions. And finally, in this video (also by Casey Muratori), he talks about how to use compression driven programming throughout a project to work towards an unknown goal. (That video is kind of long, so I deep linked the relevant discussion, which lasts about 10-12 minutes.)
The alternatives here certainly aren’t mutually exclusive. In fact, I think we can take key ideas from each to become more effective and efficient at creating code. A simplistic but useful way to look at these would be to say that Semantic Compression tells us how to identify code and data that’s worth abstracting. A Component-Entity-System approach gives us a flexible framework for organizing those abstractions without getting tangled up in an inheritance hierarchy. And Data-Oriented Design gives guidance on how to work with the abstractions in a straightforward way that works well with memory.
OOP probably has its place somewhere. Maybe in GUIs? The bottom line, of course, is to do what works best for you. Just don’t let conventional wisdom lead you down a rathole of theoretical correctness when clean, working abstractions can be had much more simply.