[C++] Avoiding god class

Started by
12 comments, last by stonemetal 14 years, 2 months ago
I've been studying game programming for about 2 years now, and during that entire time I have not yet learned how to "properly" design and structure my code. I've made three games so far and none of them left me with even the slightest satisfaction because I knew what they looked like under the hood. Two of these games were made using what I've come to know as a "god class". Basically we had a CApplication-class which pretty much created everything in the game, including all the manager-classes, entities and whatnot. Then we made an extern variable of that and named it g_pApp. This allowed everything to access everything through g_pApp (assuming we made a get-function for it, which we did for pretty much everything). Circular dependencies were taken care of using forward declarations, so that never really became a problem. Game progression was then handled using a simple switch-case in CApplication. So pretty much the entire game ended up in one single gigantic class. It made for some easy coding though, which I'm not afraid to admit, since you had this central hub which everything could route through and you pretty much never had to inject pointers into anything. I have since then scoured these forums quite a bit and realized that I really need to change the way I think about designing code. I've also read pretty much every Singleton-/globals-thread I could find, and here's where the problem lies. I know that this is a subject for heated debates, but I'm having trouble accepting the reasons for not using globals. The strongest arguments against using globals I've found are: - Spaghetti code - Difficult to refactor - Bad OO-practice - Screws with threaded applications - Makes unit testing difficult I can't say I really experienced any spaghetti code in the above mentioned projects. At least not anything that couldn't be rectified using state-classes to handle progression or something along those lines, but my definition of spaghetti code may be somewhat different than yours. And I've never actually used threading or unit testing in a game yet, and don't expect I will in a foreseeable future. So if I dismiss those arguments as non-issues, what reasons do I have for not using globals? It all starts to feel like a cult of sorts. I know I should be doing this and that, but why? Just because someone told me to? Even popular libraries like OGRE and CEGUI uses Singletons heavily without apparent shame, so why shouldn't I? This ended up being sort of a rant, but all I'm asking is for someone to point me in the right direction. I strive to become a better programmer, so how do I structure my code so that it doesn't involve injecting loads and loads of pointers to every class but at the same time doesn't involve global variables? Maybe some of you know of a smaller open-source project I could take a look at just to get some inspiration, or maybe a book I could read?
Advertisement
Quote:Original post by Spoonfork

And I've never actually used threading or unit testing in a game yet, and don't expect I will in a foreseeable future. So if I dismiss those arguments as non-issues, what reasons do I have for not using globals?


None.

Quote:It all starts to feel like a cult of sorts.

No, just the people who talk about this kind of stuff have different requirements:
- Teams of 5-50
- Code that is reused from 3-30 projects, some 5-10 years old even
- Absurd delivery deadlines
- Requirements changing weekly, even daily
- Working with junior programmers with next to no experience
- Maintaining code for several platforms
- Need to use multi-threading and concurrency

All of these imply that code used has been touched by hundreds, perhaps even thousands of people total, that compilers and OSes have changed in between hence unit and other automated testing is a must.
Rapidly changing features and prototyping, as well as being open towards large number of third party libraries imply that code must be loosely coupled, easy to refactor and heavily verified by unit testing.
As soon as multiple compilers and platforms are involved, linking details become important, which means that global state can quickly expose obscure flaws and unexpected crashes and memory problems. While these don't take long to fix, they tend to regress quickly.
Build times become an issue as well, so it is always desirable for functionality to be loosely coupled so it can be tested and developed in complete isolation.

Quote:I strive to become a better programmer


Join a team in form of a job. Work with real world code, on projects with deadlines, using fixed budgets and real people, and issues this all brings about.

Do not use open source, spare time and hobbyst projects as a reference of design. They can afford anything. Best practices usually implies professional development (aka be paid to developed on budget and on time) and that almost always involves large teams and is mostly non-technical challenge, having more to do with people involved. And this means that God classes are sometimes appropriate as well.

The design methodology you choose should fit the job that needs to be done. In general, for most projects that involve teams, the original guidelines hold, but god classes and similar might still be used if appropriate.
These may not be significant reasons for you.

For convenience: I find it a pain-in-the-butt, when I change the CApp global, to recompile unchanged code which includes the header for the global. Yeah, it may only be a few seconds, but when I'm chasing a bug or logic problem, it's a bit of a pain, and I find the time to structure my classes to avoid globals is less than the accumulated recompilation time. Just my preference.

Capability to reuse classes: I reuse classes quite a bit. If you don't, then that's a moot point. I'm not telling you anything you don't already know, but reusing classes tied to a CApp global either requires recoding when used in another app with a different CApp, or the new CApp has to have identical function calls as the previous.

With regard to having to inject "loads and loads of pointers," that may be a result of circular reasoning. Your classes may require those pointers because you originally coded them to use the global. Rethinking the structure of your classes may avoid that. Again, if you don't reuse classes or mind the compile time, that's moot, also.

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

If I were to diagnose what is happening, I would hazard a guess that your game has a "core" which contains various hardware and software resources, such as sound contexts, windowing contexts, input etc. You can only have one of these at a time, so you expose them as globals. This makes sense. All this talk of not using globals, "Bad OOP Practice" and "Spaghetti Code" is subjective, nobody can present you with logical reasons to spend time thinking up a good architecture.

But you are falling into a fallacy. Just because dependencies of the renderer, the sound manager, and other classes are singletons, that doesnt mean those classes must also be singletons. They can have internal state, you can have specialised, subclassed versions. You can even have instances which are transient, e.g. only used during a cutscene.


My sound mixer is constructed as such:

// general purpose sound mixer:
new SoundMixer( core->getSoundContext() );

// sound mixer for dealing with music (which im streaming from disk):
new StreamingSoundMixer( core->getSoundContext() );


You can do this with renderers. Why do you have to have a single global renderer which cannot be constructed multiple times?

// grab a new renderer
new Renderer( core->getRenderingContext() );

In fact, the same applies to most of the classes that people believe should be globals or singletons. Their dependencies are singletons, yes, but that doesnt mean the entire class has to be a singleton. I personally hate singletons, every time im exposed to them I get into trouble, and globals are a similar problem. My game core is not a singleton, but rather injected into the first level of GameState, but there is no need for you to do that; simply keep your "must be single" classes within your CApplication class, and move the vast majority of classes which dont have any such restriction out of there.
Don't thank me, thank the moon's gravitation pull! Post in My Journal and help me to not procrastinate!
Quote:Original post by Antheus
Do not use open source, spare time and hobbyst projects as a reference of design. They can afford anything. Best practices usually implies professional development (aka be paid to developed on budget and on time)


I beg to disagree. And, open source projects have paid developers too, and deadlines too. *Any* code can be bad/good, poorly/nicely written, have excellent/crappy design. "Best practices" usually implies writing good code, not necessarily professional development. That also includes when to cut the corners, although too tight deadlines effectively make these corners cut too sharp, which I wouldn't call a good coding practice.
Quote:Original post by u
Quote:Original post by Antheus
Do not use open source, spare time and hobbyst projects as a reference of design. They can afford anything. Best practices usually implies professional development (aka be paid to developed on budget and on time)


I beg to disagree. And, open source projects have paid developers too, and deadlines too. *Any* code can be bad/good, poorly/nicely written, have excellent/crappy design. "Best practices" usually implies writing good code, not necessarily professional development. That also includes when to cut the corners, although too tight deadlines effectively make these corners cut too sharp, which I wouldn't call a good coding practice.
Obviously there are paid developers working on open source software, that wasn't the point. The point is that if you join an open source project as a "contributor" then you're not bound by any deadlines or any of the other things that a paid developer is bound by. The same is true of hobby projects (I've totally rewritten large swaths of my game a number of times: something that's just not possible in a commercial environment)
You might find these articles helpful. I have certainly found the ideas in them to be so.
  • The Single Responsibility. Though I've been told otherwise, I can't escape the feeling that it's one of if not the most important principles of OOD. Just keep in mind the idea of sub-responsibilities, which one class will delegate to other SRP-satisfying members.

  • Principles and Patterns of Object-Oriented Design. This one covers not only class-architectural issues but packaging concerns.

The problem with globals and singletons is that they tend to scatter themselves all over the code, and then are difficult to track down when uses need to be changed, not to mention adding compile-time and semantic dependencies on the global object to most of the code. So it's really not a fundamental problem with globals, but rather with the lazy style of design they make so easy, which ultimately results in rigid, hard-to-change code. I'm not above using a few globals myself, but they're almost always restricted to a single file, and I'm prepared to factor them out if I need to make significant changes to how the object is used.

As for singletons, IMHO they have almost no place in a multi-paradigm language like C++. They are just unnecessarily restrictive. If you want a global object, make one. If you want it to be instantiated on first use, then make it a static variable in a function, what I call a lazy global. If you only need one (at the moment), only make one. Unless it is actually an error, some sort of horrible contradiction, to have more than one of the object, a singleton is just cruft.

But then, in a small enough project, it may be more worth your time to just get the thing done than to worry about issues of flexibility that may or may not actually show up before you finish the game and move on. I tend to be a stickler for doing things "The Right Way", but the same practical reasons that ease my conscience when I use a global in a single file may let you use it on a larger scale, if it doesn't cause any real issues.
Quote:Original post by Spoonfork
I've been studying game programming for about 2 years now, and during that entire time I have not yet learned how to "properly" design and structure my code. I've made three games so far and none of them left me with even the slightest satisfaction because I knew what they looked like under the hood.
As an hobbist, I hope I can somewhat understand your feelings and share some of what I've experienced over the years. After a couple of years spent in learning programming (it was probably somewhere around mid-90s) I started doing increasingly more complicated things. Most were never released for the reason you write about. A few were released and lost in the wild. Some actually generated some feedback, most just went dead.
All those systems featured the "god class" you mention in some form or the other. In some cases there were multiple gods. This was terribly boring as a large part of the systems had to be adapted from a project to another in sometimes subtle ways, not to mention to remove/add the application-specific functionality.
Quote:Original post by Spoonfork
This allowed everything to access everything through g_pApp (assuming we made a get-function for it, which we did for pretty much everything). Circular dependencies were taken care of using forward declarations, so that never really became a problem. Game progression was then handled using a simple switch-case in CApplication. So pretty much the entire game ended up in one single gigantic class. It made for some easy coding though, which I'm not afraid to admit, since you had this central hub which everything could route through and you pretty much never had to inject pointers into anything.
The closest thing I ever did was to have a very similar game state object bouncing around. It's a method of doing things which, AFAIK, has been relatively popular back when the games were simple, I know at least a (noncommercial) game which is still doing this. I don't quite understand your need to inject pointers around... I believe you're already in need of more power, yet failed to realize The Right Way to do this. I cannot quite say what TRW is for you, but for me it went thuru heavy use of virtual function and 'interfaces'. Documentation also helped me alot. If I couldn't explain the behaviour in a page without jumping thuru hoops, then the interface was probably broken. Some components designed with that metric have demonstrated to be pretty healthy.
Quote:Original post by Spoonfork
I know that this is a subject for heated debates, but I'm having trouble accepting the reasons for not using globals. The strongest arguments against using globals I've found are:

- Spaghetti code
- Difficult to refactor
- Bad OO-practice
- Screws with threaded applications
- Makes unit testing difficult

I can't say I really experienced any spaghetti code in the above mentioned projects. At least not anything that couldn't be rectified using state-classes to handle progression or something along those lines, but my definition of spaghetti code may be somewhat different than yours. And I've never actually used threading or unit testing in a game yet, and don't expect I will in a foreseeable future. So if I dismiss those arguments as non-issues, what reasons do I have for not using globals? It all starts to feel like a cult of sorts. I know I should be doing this and that, but why? Just because someone told me to? Even popular libraries like OGRE and CEGUI uses Singletons heavily without apparent shame, so why shouldn't I?
You take me by surprise. I can understand your position, but hell no, I cannot agree on having singletons or globals as a need. Globals make the scope of each piece of machinery harder to understand. As such, the side effects are not always foreseeable. When stuff starts to be complex, you have this black mass of links in your head weighting like 1ton on your effort for the next step. No matter how hard you try, you will always be able to carry it on only up to a certain point. Singletons may come in handy. Globals might cut it. God class might eventually be used, and even worse things might be needed, but no, that should be your last resort, not your starting point for designing your next iteration.
Quote:Original post by Spoonfork
This ended up being sort of a rant, but all I'm asking is for someone to point me in the right direction. I strive to become a better programmer, so how do I structure my code so that it doesn't involve injecting loads and loads of pointers to every class but at the same time doesn't involve global variables? Maybe some of you know of a smaller open-source project I could take a look at just to get some inspiration, or maybe a book I could read?
I would like to have a better understanding of why do you feel the need to inject pointers into classes... what does it means in the first place? You mean passing hooks to the engine?

Previously "Krohm"

Very interesting and much appreciated answers, I have to say.
Quote:For convenience: I find it a pain-in-the-butt, when I change the CApp global, to recompile unchanged code which includes the header for the global. Yeah, it may only be a few seconds, but when I'm chasing a bug or logic problem, it's a bit of a pain, and I find the time to structure my classes to avoid globals is less than the accumulated recompilation time. Just my preference.

Capability to reuse classes: I reuse classes quite a bit. If you don't, then that's a moot point. I'm not telling you anything you don't already know, but reusing classes tied to a CApp global either requires recoding when used in another app with a different CApp, or the new CApp has to have identical function calls as the previous.

With regard to having to inject "loads and loads of pointers," that may be a result of circular reasoning. Your classes may require those pointers because you originally coded them to use the global. Rethinking the structure of your classes may avoid that. Again, if you don't reuse classes or mind the compile time, that's moot, also.

I can definitely relate to that. I haven't been reusing classes that much yet, but I definitely see your point. Your last bit is pretty much why I'm asking you guys in the first place. I realize that you can structure your program in a way that barely require any sort of pointer-shuffling. All I want to know is how to achieve that, but I realize that there's no one-size-fits-all solution.
Quote:You take me by surprise. I can understand your position, but hell no, I cannot agree on having singletons or globals as a need. Globals make the scope of each piece of machinery harder to understand. As such, the side effects are not always foreseeable. When stuff starts to be complex, you have this black mass of links in your head weighting like 1ton on your effort for the next step. No matter how hard you try, you will always be able to carry it on only up to a certain point. Singletons may come in handy. Globals might cut it. God class might eventually be used, and even worse things might be needed, but no, that should be your last resort, not your starting point for designing your next iteration.

My way of structuring code is very primitive (hence the thread), so I realize that my way of thinking may not be the most logical or even simple for that matter. The whole pointer-shuffling craze lies in the idea that if you were to get rid of the god class, you'd have to inject all the different subsystems (which you could easily access through the god class) into every entity or manager that needed them. This might still not make very much sense to you since our frames of reference when it comes to engines might differ. But like Buckeye said, this is most probably a flaw in my way of thinking, since the god class method is the only one I've been taught so far.
Quote:
The whole pointer-shuffling craze lies in the idea that if you were to get rid of the god class, you'd have to inject all the different subsystems (which you could easily access through the god class) into every entity or manager that needed them.

The flaw in your conceptualization of good design -- if, as I am understanding from your post, this quote summarizes your aversion to dependency injection -- is that the problem in a design where you need "to inject all the different subsystems (which you could easily access through the god class) into every entity or manager that needed them," is that "every entity or manager that [needs] them."

This issue is not solved by a singleton, global or god class in any form. It is, in fact, hidden by such a thing. Explicitly passing the dependencies to everything, however, exposes the problem. Everything should not need access to everything else; it's possible (and desirable, often) to create systems where dependencies are only passed in through one or two layers of responsibility.

This topic is closed to new replies.

Advertisement