In object oriented programming, it is commonly said that each class/object/instance should have a well-defined purpose. For example, there may be a Window
class in a windowing toolkit, a ChunkLoader
class in a Minecraft-like game, or a Matrix<>
class (template) in a linear algebra library. In each of these cases, the class has a specific job. In these examples the jobs, respectively, are to handle the underlying windowing api and expose useful functions, load and unload chunks, and represent a matrix with useful operations.
While this guideline (single purpose for each class) is good advice in many cases, it seems to fall flat for the glue parts of a program. That is, something needs to call the chunkloader, matrices, or window classes as needed. This can be shuttling data between them, setting them up with the appropriate polymorphic interfaces between them, or a host of other things that combine the aforementioned parts (ChunkLoader
, etc.) into a whole. For example, in a Minecraft-like game something needs to handle the main loop and call the other classes with more defined purposes as needed.
While I am working on my programs, my first (and, so far, only) idea is to create classes that abstract larger areas of the program, and that deal with the less abstract classes inside them. This, however, tends to take the form of manager classes, in name or otherwise. For example, there may be a RenderingManager
class in a video game that has a data member for each general part of the world (ie. a member to render the world itself, another for other players, and another for GUI). Then, within it, the GuiManager
class that has its own members for various parts of the GUI (eg. inventory, health, pause screen).
This recursive manager structure, however, has several problems:
- Managers (whether or not they are called managers) are bad practice in and of themself, according to the research that I have done before asking.
- The manager classes are necessarily a particularly complex part of the program, as they touch many (or all) parts of the program, and thus need to keep in mind the idiosyncrasies of those parts. This can be partially mitigated by having clean interfaces to each layer of abstraction, but not all problems are always amicable to clean interfaces.
- The manager classes are by their very nature hard to unit test, as, at the upper levels, they rely on huge swaths of the rest of the code. This requires essentially a complete program to run unit tests. Using polymorphism as a mitigation is undesirable as the interface to each manager class should completely hide the types with a lower level of abstraction, thus making passing pointers to other types impractical. Additionally, in languages such as C++, polymorphism isn't available with basic members, meaning that the already-complex manager classes get more complex in order to enable unit tests.
- This hierarchy of manager classes can be represented by a tree where each node is an instance, with a main manager as the root and the sub-managers as its children, and so on down to the most basic classes. Sometimes, it is necessary for a node to communicate with its cousin (eg. for the renderer to query the physics engine in a video game). There is no clear way to handle this in this design.
Is there some better technique for the large-scale organization of a program and/or to glue together the more basic OOP parts? If so, what is it? If not, how can I mitigate the aforementioned problems?
getBlockAt
type thing. – john01dav Mar 23 '19 at 22:07