Are classes evil?

Started by
32 comments, last by mumblyjoe 15 years, 7 months ago
Don't dynamically allocate when it isn't necessary:
int WINAPI WinMain(...){    Application app(...);    return app.Run();}
Advertisement
Quote:Original post by smr


smr is right, although I don't agree with his conclusions. It is entirely useless to think for days on end about what the perfect design is—also known as Analysis Paralysis—if that doesn't result in a working game that's done before the deadline. You always have deadlines, even in amateur projects : you don't live forever, and your motivation is bound to decline a long time before you die anyway. So, always strive to strike a balance between design and accomplishment: too much design leads to no accomplishment, too little design leads to accomplishments that involve more rewriting old code than writing new code. However, if you've been there and done that and burned yourself with object-oriented design, the correct conclusion is not "object-oriented design sucks" but "I've been doing object-oriented design wrong".

I personally design on the fly, using the basic tenets of:
  • Use Idioms : the closer you stick to what is the "standard way of doing things" in your language/library, the more features of the language/library you will be able to use without any work.

    This obviously requires you to know what the idioms are, but it requires no design effort when you're writing code.

    Most people usually think merely of code style when I say this, but it covers a lot more. It also means that you should use the standard library containers whenever you need to represent mutable sequences and iterators for immutable sequences, that you should use RAII and respect the rule-of-three for constructor-assignment-destructors, that you should use constructors instead of factories whenever possible, that you should use references instead of pointers whenever possible, and so on.

    Derive your own idiomatic rules for your own language, if not C++ (I could rant about Objective Caml idioms for just as long).

  • You Ain't Gonna Need It : don't implement functionality unless you need it for what you're doing right now. If you don't know yet what you will need, your code will either be thrown away or will have an interface that's not adapter to how you need to use it.

    This is of course different from not writing code: code is cheap (easy to write, fairly repetitive, and compiler-corrected), functionality isn't (you need to decide how to do things, then test and debug). Glue code, for instance to respect idioms or to ensure extensibility down the line, is even cheaper.

    For instance, if you're writing a transform system, you already know ahead of time that several functions (rotate around arbitrary axis, for instance) can be implemented, but actually implementing them needs both intellectual effort and time. So, unless you have a clear example where the function will be used (and how it will be used) don't write it yet.

  • One Step at a Time : leaving one part of the program unfinished in order to complete another part means the first part will contain bugs (because it was implemented in two steps) and the second part will be highly coupled to the first.

    If you're writing some bit of code and suddenly find that you need another bit, there's a quick design question that you need to ask yourself : does this coupling (between the thing being implemented and the thing that is required) need to be strong ? Your choices are making the missing entity a parameter (that will be provided by the user of what you're currently writing) or writing a quick mock of the entity and globally referencing it. Depending on what the entity is, the choice will go towards parametric or global referencing. For instance, it's seldom advisable to keep global references to mutable data or anything that could be considered a configurable option.

  • Separate Responsibilities : if an atomic piece of functionality is bound to another piece of functionality (so that you can't use one without the other) then you need to separate them or you'll suffer down the line when one is needed without the other.

    The basic rule to determine this is to look at your design and look at every individual functionality, and decide whether that functionality can be used without any overhead (having to create objects or variables not directly related to that functionality). For instance, having to create and provide a texture manager (or a null pointer, by the way) to determine the number of enemies in a video game level is nonsense, meaning that the object or module responsible for accessing level data should not require a texture manager.

  • Once, Twice, Refactor : if you notice any repetition in your code, refactor so that the repeated code is repeated exactly once. The result will be code that is smaller (thus easier to understand and debug) and the refactoring will identify frequently reused semantic actions that may be useful (if you reused that code once, you will probably have to reuse it again).

    Don't be afraid, if you notice after writing a module that another design would have been much better, to either mention this in the design (as a comment) or to refactor it right away. The module should be small enough to allow you to refactor it within an hour.

  • Test First : automated unit testing serves two purposes. The first is that you are now free to manipulate code on a larger scale (such as massive refactorings) while still having a safety net that detects any mistakes you make.

    The second is that every module in your code now has two users. Several users means better debugging (you notice the errors earlier if more code uses what you just wrote) and also better design (you'll have less coupling, and you might notice awkward constructs while writing the tests).

    Almost everything in an application can be tested using unit tests, except the results of your API calls. If something seems difficult to test, this indicates that your code is difficult to reuse and lacks modularity, which will inevitably lead to issues down the line that are better solved right now.

  • Slim Down Your Interfaces : whenever you use an object in a function, think of the smallest set of functionality that is required from these objects. Anything you don't need shouldn't be required. This extends the range of situations where that code can be used.

    For instance, if a function is responsible for moving a camera around in the scene, that function has no reason to require that the passed object is a camera, because the fact that the argument is a camera is not used anywhere in the description of the functionality (aside from the arbitrary requirement that it should be one). The correct requirement here would be that the moved object should be an object which has a position within a scene (something which is the case for a camera, but also for most other objects, such as meshes or lights).


Classes are useful tools to achieve several of the above. If you know how to use them appropriately, then use them.
ToohrVyk thanks for taking the time to write this down. It's very decent advice indeed, one thing though I did not understood. What do you refer when your speaking of automated large scale unit tests?
Quote:Original post by McCoder
What do you refer when your speaking of automated large scale unit tests?


Unit tests, test-driven development and game applications.

The basic idea is that you design your program functionality by functionality, for instance deciding that you want to code your enemies so that they fire bullets. So, you write some code which uses the enemies and checks that they can indeed fire bullets (meaning that they trigger the fire-bullet action and see if it did the right thing, and then tell you whether the test was successful or not). Once the testing code is written, you write the interface of the actual code to fire bullets (so that the code compiles, but the test fails because the bullet-firing function does nothing), which shows that your tests work. Then, you implement that function, and check that the tests still compile and also succeed.
Quote:Original post by McCoder
Looking at the flow of win32 api and opengl makes me wanna stick everything into classes. Is this a good practice? I would want to get so clean that I should read InitSound(); DrawStuff(); DiePainfully(); etc. Good clean code.


The arguement against classes has been around for as long as I can remember; it was probably a bit more valid back in the Pentium I days. Classes are probably one of the smaller speed concerns. Poorly written code (or poorly understood problem), as always, is a bigger concern.

Also, keep in mind that many of the APIs (Windows, MacOS, OpenGL?) were around before there were solid C++ compilers, and were also designed to be used from other programming languages (asm and pascal, for example).

Check out Super Play, the SNES inspired Game Engine: http://www.superplay.info

Quote:Original post by smr
A lot of people are going to come in here and tell you how not to put this code in a class. They're going to tell you to follow the single responsibility principle, they're going to stress encapsulation, data hiding, and proper OO paradigms. Most people get so caught up in designing their program in the proper OO way that they never actually finish something. That's why many people are on their second, third or even fourth rewrite of their game-- er, engine.

For a small project like an arcade-style game, things like proper OO design and principles are just going to keep you from your goal of creating a working game, especially if you haven't a lot of experience finishing games. My advice is to wrap that stuff up in classes if it helps you get your game done. You might step on your own foot a few times, but you'll learn and for your next game you'll do it better. And you'll be three steps ahead of us who're on our third engine rewrite.


Great point, I've had this happen over the last couple of years; I've started focusing more on writing 'great' code, rather than actually developing software. Small advancements in code usually drive development; you can't see advancements when all your time is spent creating classes and interfaces. But, once you've got something nailed down, take time to refine (refactor) the software. Part of the interative process should be to clean up the interface.

Check out Super Play, the SNES inspired Game Engine: http://www.superplay.info

Quote:Original post by ToohrVyk
...


I agree 100%. In fact you've pretty much outlined exactly how I develop on my own. I believe that with experience and the proper amount of introspection, any developer will come to follow most of these guidelines. Now, I wasn't attempting to recommend that the OP completely forget about design. I was just trying to inject a little anti-PATTERNS!PATTERNS!PATTERNS! before the YAY!OOP! chorus. I recognize the utility of patterns and apply them when I think I should, and I definitely appreciate good object oriented design, but, in my experience, finishing something in a less-than-perfect way is far more educational than not finishing something in a perfect way.

I always enjoy your posts, ToohrVyk!
Quote:Original post by rip-off
Don't dynamically allocate when it isn't necessary:
*** Source Snippet Removed ***


So the Run method would contain the WinMain stuff and the constructor ... basicly nothing.
Quote:Original post by McCoder
Quote:Original post by rip-off
Don't dynamically allocate when it isn't necessary:
*** Source Snippet Removed ***


So the Run method would contain the WinMain stuff and the constructor ... basicly nothing.


That is up to you. I would put initialisation in the constructor, cleanup in the destructor and the "run" stuff in run. But putting everything in an Application object may not be necessary, I don't bother doing that myself.
Nope he just says allocate the app class on the stack instead of dynamically allocating it.

The message loop will be inside the app::run method.
Here is an example from my code maybe it makes it clearer:

int WINAPI WinMain(...){    WndApp app(...);    return app.messageLoop(..);}

Then you have a message loop like this in the App class
 WPARAM WndApp::messageLoop()    {	    MSG msg;//call your init function	    initApplication(m_hInstance);	    			            while(m_bRunning){//handle messagees		    if(PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)){			    if (msg.message == WM_QUIT){				    m_bRunning		= false;				    			    }			    TranslateMessage(&msg);					    DispatchMessage(&msg);		    }		    else{			    if(WndApp::m_ApplicationActive == false){				    WaitMessage();			    }			    else{//draw,update etc.				    mainLoop();			    }		    }	    }	    return (msg.wParam);    }

Edit : yeah the initApplication method can go to the constructor of the application class.

This topic is closed to new replies.

Advertisement