Sunday, September 6, 2009

Singletons, Static Classes, Namespaces?

Global Objects Are Evil (?)
It is a widely held opinion that global data in code is evil. This belief dates back to the days of C, and is no less true today for C++ programmers. Why is it evil? The standard input, output and error file descriptors are global. In fact, there are many examples of global data in the C runtime library and the standard C++ library (e.g. iostreams). If they are so awful, why are they first-class citizens in these languages? Maybe not all globals are evil, but practical experience of battle-hardened programmers shows that they are usually evil in application code.

So what are the perils of globals?

To start, the standards, implementations and platform details are too vague to support static global data in anything more than the most trivial applications. Video games are the furthest thing from trivial programs.
  • Object lifetime is difficult to manage. C and C++ have well defined behavior for static initialization and destruction through atexit(), but dynamically linked libraries and other application-specific code can unwittingly violate the complex interactions of several objects in an application. The last static Renderer destroyed might not be the one that was first created. It certainly won't have the same object/game state associate with it when it is accessed on its second incarnation!
  • Some platforms may actually produce more than a single instance of a static object depending on the context that references it. Windows platforms that delay-load DLLs is one (of many) examples where static objects may be instanced and initialized more than once.
  • Nothing in either the C or C++ standard can define the behavior of multiple threads attempting "first access" to static global data.

Globals + Project Schedules Break Encapsulation and Reusability.

Because games are such complex, expensive and time consuming endeavors, the realities of budgets and project timelines create even greater havoc for static global data. Most game studios ship more than a single title before evaporating. After a product has shipped, and as the new one ramps up, there is pressure to recover the time and effort spent creating technology for the previous product. The programmers involved with shipping the last title often champion the technology they have spent the last 1-3 years developing and encourage the new team to hit the ground running for the next game with their "proven" technology. For the most part, the bits that are tucked furthest away from the game code they produced is ripe for re-use. The untold part of the story are the global objects that were polluted with game-specific code for an E3 demo, or a milestone review, or during the last 90 days of the project where programmers on a death march just wanted to ship the damn thing and take some vacation.

In game development, if the deadline looms, and a shortcut is available to ship a successful title, it will be taken. Global data is the most convenient shortcut to get the most important job done. Writing proper scaffolding between a Renderer and a GoblinObjectManager could take days when a programmer is left only with a few critical hours to ensure the success or failure of the project.

When the next team picks up the code and tries to use a design (that has nothing to do with Goblins or GoblinObjects), they find the global rendering object has been polluted with piles of code that has less to do with rendering in general, but everything to do with shipping the previous title and thus ensuring the studio still exists to employ them. This was the right decision to make given the circumstances and code they had to work with! Without the business requirements to meet, there would be nothing to sell and no reason to write the code in the first place! Those programmers that got the job done are the guys that ensured everyone else has a job to do for the next project. Always remember that "end of project" priorities are nothing like "beginning of project" priorities. They are often at oposite ends of the programming spectrum.

The new team, however, faces months of refactoring code and extricating the E3 demo garbage and last minute additions before they can really get started on finding the "fun" in their title. It would be cleaner and easier to simply start from scratch, right? If you are a game developer and can name this tune, raise your hand now!

C programmers in other disciplines already arrived at this conclusion on their own projects. C++ game developers that have lived through several product life-cycles have experienced this first hand. This is why there is a wide-spread opinion that globals are evil.
  1. The standard doesn't sufficiently address common usage of global data in complex applications.
  2. It is very difficult to manage initialization in multithreaded environments.
  3. Object lifetime is to easy to violate producing unexpected results.
  4. Globals make it too easy to break encapsulation, thus wasting time investments spanning years and preventing code re-use (the mantra of good OO design).

Coding "by convention" can solve one ore two of these problems. Convention never solves all of these problems, even on the most disciplined technical teams. It is always better to use features of the language to communicate the intent of technical design. Documents and convetions rarely solve the problems sufficiently.

If it can be agreed that the use of global static data is almost always a bad idea, how can single-instanced, global access to important application data be achieved? That is, afterall, why anyone would even ask whether a Static Class, Singleton, or Namespace would be appropriate to solve a problem, right?

Singletons and Static Classes are global data. They exhibit the very problems just discussed (and many more). If readers disagree, comment on the post and we can explore it further in future treatments :) If it is agreed that static classes and singletons represent global data, read on to the next section.

Just Don't Do It
C and C++ programmers worth their salt will first look for solutions that don't require globals at all. Many window toolkits and other complex systems simply pass contexts around and use parent-child relationships to form a well defined, acyclic dependency graph of objects. These systems demonstrate clean design and lend themselves to re-use more easily than those that lean on the crutch of access to global data. In C, it was often considered good design to pass opaque pointers to context structures through client code. While this may have bloated intermediate interfaces, the coupling between context data and higher level systems was properly communicated.

One problem (of many) with this approach is that any system built around passing a monolithic context could not easily be re-used or re-factored if the context were broken into smaller component parts. If, for example, a Renderer were to separate the notion of drawing meshes and defining camera space, access to the camera space could not be separated while still supporting the older, monolithic code, even if most of the code never bothered with the camera.

An Extendable, Global Interface (Or Do as the Runtime Does)
There are some solutions to the multi-threading problems of globals (double-locked singleton initialization). There are some problems solved to deal with broken encapsulation (just don't use globals). There are solutions to object lifetime issues (explicit Install and Remove methods for globals). What is really needed is an approach that doesn't expose global data, can manage data initialization through an interface, do it in a thread safe way, and allow game programmers to extend functionality without polluting the core interface or implementation!

C++ introduced Namepsaces, which a lot of old-school C programmers regarded as a "hack". Namespaces, however, provide the path to the solution to all of these problems.

Static classes and Singletons are static globals. I would hope it has already been demonstrated that static global data is the choice of last resort. Even if the initialization, lifetime and threading technical problems are solved in a static or singleton implementation, the fact remains that they break encapsulation in practice when they are polluted with application-specific code required to ship the product. Even the most conscientious programmers have no alternative, because static classes and singletons do not provide partial closure semantics -- e.g. extending the base functionality through new client code that is separate from the original implementation.

The brilliance of namespaces in C++ is that the can provide special closures.
Renderer.h
namespace Renderer
{
    void drawMesh(const Mesh & mesh);
}
GoblinRenderer.h
namespace Renderer
{
    void drawGoblin(const Goblin & goblin);
}

The Renderer interface has been extended deep within the game, and available only to the game to provide a rendering interface that makes sense for the game. The core Renderer interface escapes un-polluted with the game-specific rendering requirements. The renderer has been extended beyond it's original interface closure into the game. When the team evaluates the renderer for re-use apart from the game, the core code is not decorated with game-specific interfaces to support implementations required to ship the product!

Namespaces aren't a panacea. Programmers need to remember that the basic technical complications with object lifetime and multithreading issues need to be solved for ALL globally accessible objects, even if they are accessed through a namespace.

If a namespace is used, it is preferable to solve the object lifetime problem first, using Namespace::Install() and Namespace::Remove() explicitly in the code, usually after main() has been reached in execution. In the Install() and Remove() namespaced functions, the double-locked checks for dealing with global data should be honored. These two approaches solve the technical issues surrounding the "singleton problem". The fact that they are implemented in terms of a namespace instead of a monolithic object address the very real, pragmatic issues facing game developers on every project.

If the code requires global access to something, first attempt to design it in terms of a namespace. If that is not possible, abandon all hope!

No comments:

Post a Comment