Coming from a background in math/algorithms/analytics, object-oriented programming didn’t really sink in until I had to develop a large model that spanned multiple levels of scale.
My initial approach was to create classes full of static methods to execute computational procedures. Stateful objects only existed at the lowest level of scale, and procedures carried these objects around and updated their states.
(highest level of scale) ___________________ | | | | | | | P | P | P | P | P | | R | R | R | R | R | | O | O | O | O | O | | C | C | C | C | C | | E | E | E | E | E | | D | D | D | D | D | | U | U | U | U | U | | R | R | R | R | R | | E | E | E | E | E | |___|___|___|___|___| | | | LOW-LEVEL | | STATEFUL OBJECTS | |___________________| (lowest level of scale)
Under this approach, each procedure was confined to its own file. Each procedure file was like a recipe in which low-level computations were combined to produce some emergent high-level behavior. Because all details about the recipe were in a single file, the recipe could be fully understood without having to reference other files.
As the code base grew in size and complexity, though, this procedure-based architecture led to an explosion in the number of parameters that needed to be passed throughout each procedure. In turn, this led to an explosion in the amount of information that I had to keep in my head during development, or equivalently, an explosion in the number of non-obvious bugs that could arise.
What ended up working better was creating a stateful object for each tangible entity at a particular level of scale, and distributing cross-sections of the procedures across those objects (as methods).
To carry out a procedure, a high-level object would perform high-level reasoning about the interactions between its mid-level objects, but it would not worry about any details that could be handled within a single mid-level object. Instead, it would pass off the responsibility to the mid-level object itself – which would in turn perform mid-level reasoning about the interactions between its low-level objects, passing off responsibility directly to the low-level objects when possible.
___________________ ___________________ | | | | | | | HIGH-LEVEL | | P | P | P | P | P | | STATEFUL OBJECTS | | R | R | R | R | R | | W/ CROSS-SECTION | | O | O | O | O | O | | OF PROCEDURES | | C | C | C | C | C | |___________________| | E | E | E | E | E | | MID-LEVEL | | D | D | D | D | D | --> | STATEFUL OBJECTS | | U | U | U | U | U | | W/ CROSS-SECTION | | R | R | R | R | R | | OF PROCEDURES | | E | E | E | E | E | |___________________| |___|___|___|___|___| | LOW-LEVEL | | | | STATEFUL OBJECTS | | LOW-LEVEL | | W/ CROSS-SECTION | | STATEFUL OBJECTS | | OF PROCEDURES | |___________________| |___________________|
Although computational recipes became spread out over multiple files and objects, far fewer parameters needed to be passed around. The code was much easier to read, and it was much harder for non-obvious bugs to arise.