6

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?

  • 3
    This is exactly the kind of problem which is addressed by Dependency injection. Instead of making the "manager" classes depend on "huge swaths of the rest of the code", DI makes manager classes depend on abstractions (which can be "mocked" out for unit testing, for example). That resolves the hierarchy completely, and the only "glue code" which remains can be handled by a DI container. Communication to "cousins" can be handled by events, which can be seen as a form of DI as well.. – Doc Brown Mar 23 at 9:35
  • 1
    Well, Doc is right but one thing is missing. On top of this (or at the bottom depending your standpoint) there will be components doing the heavy lifting or gluing everything as you (OP) are guessing. If we use frameworks, these components are out of our scope. If not, it's likely we have what's known as a Composite root somewhere, which in many languages is known as a Main (class, method, whatever), Application, Bootstrap, World, etc. Somewhat like the Universe, the dependency tree starts in one point and time – Laiv Mar 23 at 14:16
  • @DocBrown Is there some way to make the abstractions something more than just an interface with the same functions that the injected classes would have anyway? If not, then I don't see how it decreases complexity. As for communication to cousins, events work in some cases but not all, it seems. For example, I need to store the voxel data somewhere, but both the rendering part of the code and the physics part of the code need to access it very efficiently, with a getBlockAt type thing. – john01dav Mar 23 at 22:07
  • @john01dav as you see the scope of your doubts and concerns are fairly too broad. Your question brings to the table design concerns that have no magic or universal solution. Without more context and details, it's going to be impossible to give valuable answers. At this point, I got the feeling that you need stop thinking in classes, interfaces, managers, patterns, etc. So, do few steps backward, see the actual design with perspective (draw some diagrams if need It) and rearrange the whole design. – Laiv Mar 24 at 9:41
  • 1
    @Laiv: yes, and the kind of glue in a "composition root" (which only resolves dependencies) does not have the issues any more of those manager classes described by the OP, because they are completely free of any business logic (I did not mention this because when writing the comment above I had only the time for a short comment, not for a full-blown answer). – Doc Brown Mar 24 at 14:50
1

Manager classes are born out of wrong abstractions, therefore any solution based on them is bound to not fit well and cause all sorts of problems, especially with OO concepts.

Let me just mention one example that I encountered this week. I was writing "subscription" handling in an Android application. Now "normally" you would see some SubscriptionManager that will give you a Subscription, that would probably contain some product id, whether it is paid, stuff like that. That is the generic abstraction of what a subscription is (some would argue).

However, what I wrote was just one Subscription class that contained a single method Content retrieveContent() (roughly).

Instead of generalizing what a "subscription" is to basically any application, I've made my own Subscription that is just a subscription in my application. I didn't have to solve subscription management for all applications, just for mine. This also means, that business concepts can be part of what my Subscription is. And in my domain a subscription's only purpose is to access some content.

This is actually true for all objects. All objects (in the application) should have some amount of business logic in them, otherwise why would you have them in the first place? All other objects (like String, Matrix, Window, etc) should have absolutely nothing to do with the application and should preferably come from libraries.

At the end I don't have things that need to be "glued" together, rather things will naturally contain each other or refer to each other along the lines of business logic (like a Subscription will retrieve a Content, instead having ContentManager.retriveByProductId() or some such).

Word of caution: Most projects (that I saw in the last 20 years) just do "Managers" and are content with mostly procedural decomposition, so you probably would be swimming against the tide trying to do something more inline with OO.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.