When OOP Works and When It Doesn’t
A key problem with the OOP paradigm is its overuse, but how do you know when to use it and when to avoid it?
Conceptually, OOP is built on the idea of a network of independent objects communicating with each other. One object asks another object to do a serve for it. Often this way of looking at the world is preferable, especially at a large scale. It is how the internet works. We have individual severs communicating with each other and doing services for each other.
But there is a problem with this way of organizing programs at lower levels of granularity. The focus on objects with identity creates problems. If you are doing things with specific identifiable objects, then this leads you down a path where objects get mutated. It is harder to analyze code where objects change state a lot. Immutable objects are easier to reason about.
Thus in many cases we don’t want think of a specific object with a particular identity existing in a network of objects. Instead we want to think in terms of data-flow. Data flows through different pipelines and gets transformed. In this paradigm, objects are transient in nature. They come out of a processing node and disappear as they enter the next processing node. You can think of a Unix pipeline as an analogy. Functional programming and Data Oriented Design both work much more like this.
In this paradigm, there is not a natural push towards mutable objects. Objects are more naturally modeled as fixed, because they only exist while they transition between two processing nodes. Instead of the OOP network of objects communicating with each other, we get a network of nodes sending streams of data between each other for processing.
This all sounds very abstract, so what is the key difference? In the OOP network of things, the nodes are objects. The communication lines are method invocations. In the data-flow paradigm, the nodes are functions and the communication lines or the edges is data flowing between these functions. Conceptually we have flipped everything on its head.
If mutation is so bad, should we build everything this way? No, I don’t think so. In many cases, the view of the world as communicating objects make a lot of sense. As I mentioned before, the internet works that way. Any time you deal with identity it makes sense. For example, I think GUIs naturally fit the OOP paradigm. A button, slider and a window is naturally expressed as objects with identity. It often works for computer games. Entities in a game naturally have identity and get mutated. You character takes damage or picks up a new weapon. Both cases involve mutating the state of the object representing your character.
But at a lower level the software may benefit from less of an OOP approach. For example, how you render graphics to screen would often be better expressed as a data-flow where data goes through various processing nodes (functions). The identity of the shapes making up a character is not important. It is just data to be processed. Likewise, the collision shapes used to check for collision between characters and the world don’t need identity. They can be part of a processing pipeline.
Here are some ways to help you think: You probably want object at the highest level. But when coding always consider whether something could be better expressed as a series of data transformations rather than as collection of objects talking to each other and mutating each other.