Great that you asked, Why? about such a fundamental aspect of programming. This is how better thinking and better tools emerge. However, classic OO-style programming languages are unlikely to change any time soon, so for you, the key thing is to classify your problem and then evaluate it according to your needs.
"Good" vs "bad" is based on what you see as most important - concision (static structure), orderly organisation of code (static structure), reduced indirection (a dynamic or runtime concern), lack of stateful side-effects (at runtime), etc. Each of these factors plays off against the others.
You'll find that static, architectural concerns are often at odds with runtime performance concerns. A more human-readable codebase (OO being the prime example) tends to be less performant, whilst more performant code, i.e. code better suited to the machine's architecture, is often a lot harder for us to follow.
This answer is long, but I want to put things into perspective for you in terms of the programming paradigms used.
Option 1
This is what OO purists advocate, as it reads like a natural language sentence.
However, it also contains potentially costly double indirections such as bob.getNeighbor().getPet();
. Certainly .getNeighbor()
's result can be cached and used multiple times in the same function, but what if you only need it once? If the object is temporarily fixed (for more than a single frame), such as a cat having a new neighbour who will stick around, then you can say cat.neighbour = bob
, to avoid making method calls to retrieve the owner
every single frame (although profiling the impact thereof, is up to you).
Over time these indirections can be eliminated in critical sections of the code to address performance, with these sections clearly marked as optimised for x and y concerns, to avoid anyone re-refactoring them for readability in future. As performance increases, the code looks less and less canonically OO.
OO incurs costs also through its vtables, which procedural languages do not suffer from.
Option 2
This is sort of a hybrid between pure OO (1) and procedural (3) approaches.
Long argument lists are a common feature of C which is the modern reference point for procedural programming; often these are packed into a single context argument (struct
or object) which is similar to this
in OO languages (only, it is explicitly given unlike this
which is implicit).
However, you are still using .findNeighbor()
and .findPet()
, which are methods, not global functions, and as such exist only in OO languages.
Option 3
This is pure procedural programming, around long before OO arrived. Functional programming is closely related, albeit that it has additional constraints around inputs and outputs.
Here you will find no this
and no somebody.doSomething()
methods; only global functions like doSomething(somebody)
. That is, where (i.e. on what object) each function is located, is not important here: functions exist in global space. This is closer to the way they are represented in the machine's instruction cache, which makes no distinctions.
Procedural programming, for games at least, is commonly used for small, inherently fast programs written in the C language (or C subset of C++). C is commonly used where you need a special purpose component that to be lightning fast - faster than primary code written in e.g. C# or Java.
Functional programming is similar to procedural, but mandates that we inject all of the necessary inputs as function or method arguments, and returns all new output(s) based purely on those inputs and nothing else, i.e. neither inputs nor outputs are based persistent state as found in this
members, or global members found at module level. This aims to reduce errors caused by state / side effects, assuring a higher degree of program correctness and debuggability.
Cost of method calls and indirections
Dot-syntax drilldown, e.g. owner.cat.bowl
, incurs costs, this is known as double indirection.
In Assembly / machine code, these cause what are called JMP
ops, which require the machine to "jump" to different location in either instruction (functions, methods) or data (members) cache, possibly causing cache invalidation, which is a major cause of reduced performance in code.
Conclusion
I'd aim for OO, i.e. 1 or 2, though between these, there is no "right way". I would go with option 1 until I saw a need for option 2, although 1 is more work as you have to write extra getter functions for each little thing (with a decent IDE, this is not so bad). Uncle Bob favours clean architecture over performance, suggesting we aim for the most concisely readable code first, and optimise only in critical sections. His background is in business programming, where maintainability comes first. In games, however, this is only where we start.
If you aim for program correctness right off the bat, and tend to have a mathematical mindset, functional programming may be preferred.
If you like C (or Basic, Pascal or Delphi) for whatever reasons, procedural may be preferred.