--------------------------------------------------------------------------------------- More Coding Notes --------------------------------------------------------------------------------------- This is a followup to my "coding style" advice doc. This doc is more about specific patterns. --------------------------------------------------------------------------------------- 1. Make Files That Work on their Own --------------------------------------------------------------------------------------- Frequently people make classes that require you to do "this over here", "that over there", etc. That's bad. Classes should do everything they need to in their own file. One common example is factories and factory-products. Often people will make a resource like : class Geometry : public Resource { public: static Resource * Factory(); ... } Then, they'll hook it up somewhere else, like : void BigGlobalRegisterOfFactoryProducts() { Register( ".geo", Geometry::Factory ); } That's bad. Much better to do it within Geometry.cpp , with something like : AT_STARTUP( Factory::Register( ".geo", Geometry::Factory ) ); Here AT_STARTUP is a neat little macro which makes a class so that the line is run at CRT init time. Here it is : #define STRING_JOIN(arg1, arg2) STRING_JOIN_DELAY(arg1, arg2) #define STRING_JOIN_DELAY(arg1, arg2) DO_STRING_JOIN(arg1, arg2) #define DO_STRING_JOIN(arg1, arg2) arg1 ## arg2 // Macro for static-initialization one-liners. // !! Only works in statically-linked .obj files. !! #define AT_STARTUP(some_code) \ namespace { static struct STRING_JOIN(AtStartup_, __LINE__) { \ STRING_JOIN(AtStartup_, __LINE__)() { some_code; } } \ STRING_JOIN(AtStartup_, __LINE__) ## Instance; }; Now, one problem with this is that it doesn't work in DLL's or libraries because the damned linker will drop those objects. One option is to turn off the option in the linker to exclude "unreferenced" objects. Another option is to put this register line in the header. That's actually what the C++ standard IOstreams do. Then you get something like this : In Geometry.cpp : /*global*/ bool Geometry_Registered = false; In Geometry.h : extern bool Geometry_Registered; AT_STARTUP( if ( !Geometry_Registered ) { Geometry_Registered = true; Factory::Register( ".geo", Geometry::Factory ) } ); Now you just have to make sure that Geometry.h is included somewhere in an object that won't be dropped by the linker. You actually don't need to do the Register() in the header, the fact that you're reffering to an externed variable will do the trick. The point of all this is that if I want to make "Geometry2", all I have to do is copy what's going in Geometry, and it will all work. I don't have to know the black art of how to hook things into the system and make them work. This is a very common pattern, and it's very nice to have all the bits in one place. --------------------------------------------------------------------------------------- 2. Pull Arguments, Don't Push --------------------------------------------------------------------------------------- With a normal function call, the caller is "pushing" arguments at the function. This usually works well, but I'm going to discuss some situations where "pulling" is better. In many cases, you have a "client" (eg. the Game) which knows a lot about the world, and it wants to call a function in a "service" (eg. the Engine) that can't make queries back to the world, because it's generic and doesn't know about the client (Game) code. Often, you'll call the same function many times in the service (Engine) with the same arguments. Some examples are collision-detection, rendering, streaming, and lots of others. I'll consider the case of rendering for concreteness. With rendering, the Game says "RenderWorld" on the Engine, and then the Engine says "Render" on lots of objects. With the standard way of pushing arguments at functions, you wind up with something like this : void Geometry::Render(const Frustum & f,const int lod,const vector & lights, ...); eg. you have to list everything that any Geometry might need to know to render. Each time you add a new Geometry that needs some more information, you have to go and change every Geometry and add those arguments. This is a big problem, and a sign of bad design. That's a good general note - in a good design, your base interfaces and classes rarely change, the frequent change happens at the leaf-module level. If you find yourself frequently changing your base modules, you probably have a bad design. So, "pull" args are better. One way to do this is with an arg-struct : void Geometry::Render(const RenderArgs & args); Now the signature of Geometry is always compliant. When we write a new Geometry type that needs some more information, we just add something to RenderArgs. One note, though, RenderArgs should NOT be a simple struct, because if you do that, you'll have to look for every caller and make sure they fill out all the values. Rather, RenderArgs should only be setup with constructors or function calls, and those function calls should require all the fields (default args may be ok). The result is that when you add something to RenderArgs, you don't need to touch any of the Geometry files. You DO need to touch all the callers of RenderWorld(), which is exactly what you want - it's good that you are forced to deal with those and fill in proper values for the new fields. That's another good general note - using naked structures and data for passing args is very bad. Any time you can add an argument and you aren't forced to deal with the callers, you have a bad design. ---------------------------------------------------------------------------------------