When men first came to the valley of Wu, they were desperate and miserable, for they had no hides to shield them from the cold winters, no wings to escape the maws of nightly beasts and no roots to draw sustenance from the barren soil. Man, a creature of craft and tools, could not function without a complete infrastructure where the miner brought the ore to the smelter, who brought the iron to the smith, who brought the plow to the farmer, who tended the fields and provided pizza and coffee to the programmer, who wrote video games for them all.
And so, the ever-pervading Titans of Wu saw the desperation of these men, and pity entered their eternal hearts. Yojimbo, the Wooden Dragon, took upon himself to bring fresh wood to every man in the valley. Mishigo, the Sparkling Flame, brought fire to every hearth, warmth to every home and overclocking to every Pentium. Soon, every Titan found its way into the daily lives of the men of Wu.
Today, my blade sought the heart of Asuna, the Physics Engine, and found it.
"Who will provide our daily simulation steps, if not Asuna?" had they asked for centuries. Men had grown too dependent on the mighty Titan, scared as they were to do the work themselves. And so, as the small valley grew to a continent-spanning empire, the presence of a single Physics Engine grew increasingly inadequate. For when the wise sages of the east wanted to run a simulation, they had to wait for the clever smiths of the west to finish their own, and such woe came to those who had forgotten their cleanup steps that most of the time was spent in repetitive multi-threading incantations rather than actual simulation.
And so the time came to slay our former ally, for only its death could bring change to the World of Men. Like wind among the snow-covered BSP trees, our secret army flowed throughout the code base, spying on Asuna's every tendril with our refactoring tools, silently replacing her all-encompassing embrace with a rogue network of ersatz Physics Engine instances that were passed hand-to-hand to those who needed them. Static type-checking ensured that no man was left behind, and a small unit-testing task force was trained for the occasion.
Until that climactic battle. For Asuna noticed, and so great was her anger that countless access violations tore apart the fabric of the empire. Our spies and networks had provided everyone with the instances they needed, and our unit tests could detect the tremors in the Titan's footsteps, but the power of the Mighty Ones was beyond our imagination—for with their fingertips they commanded the powers of Unwritten Assumptions.
So concerned had we been with providing every man and woman with their own instance, that we had forgotten to check who expected to share an instance with another: everyone had their own instance now, but everyone expected the others to be using it as well. In the utter collapse of our civilization, we fought to bring together foreigners from distant realms who happened to work with our own people. Who would have expected that two people thousands of miles apart needed to use the exact same instance?
But the fight is over now. Dry tears mourn our fallen brothers of the Global Variable Inquisition, and a black wind rises from the east. Orochi, General of the All-Seeing Logging System, has roused from his slumber.
When you're writing code, you need
to make assumptions or you will never ship anything. Some assumptions are pretty solid — "I will never have my code run on a Sega Genesis console" is as solid as it gets these days — but others are weaker. There's a whole spectrum of them actually, and you need to decide which deserve to be made and which should be avoided.
Having all your code share a certain instance is such an assumption. Sometimes, making it is
the fastest path you can follow to your objective, and it would be silly to avoid it merely because it "feels" dirty, or someone told you it's a bad thing.
What you absolutely need to, however, is prepare a contingency plan. When (not if
) that assumption is eventually broken, you will need reinforcements, because the last thing you want to do is backtrack on that assumption with your debugger
. In fact, there's an entire scale of how easily you can backtrack on an assumption:
- Toggle a compiler option or runtime configuration setting. This is as cheap as it gets, so don't fret. Make that assumption and move on. Total time: five minutes. This is where "how much stack memory do I need?" assumptions live.
- Change a definition in your code or data files and recompile. This increases the possibility of error, and it requires sufficient knowledge of the code base to know that definition was there in the first place, but it's still a cakewalk. Total time: 15 minutes, including coffee. This is where "How much damage does a rocket do?" happens.
- Use automated refactoring tools. This is where "Do we call them _foo or m_foo?" happens. Total time: 30 minutes, mostly checking that the refactoring was correct.
- Do some manual refactoring using a simple deterministic set of rules, until the compiler stops complaining. This is what removing a global variable means if you've followed proper global usage patterns (more on this in a bit). Total time (from my experience): about an hour for a 20KLOC project. I do this about once a week (and I don't have unit tests, but I have excellent reasons for doing so).
- Do some manual refactoring using complex rules, until the unit tests stop complaining. This is what removing a global variable means if you were sloppy with your usage patterns. This might take you a day, possibly more—at this level, things are not predictable anymore.
- Read the code to reverse engineer the basic principles, write unit tests, rewrite significant portions. Could take up to a week and involve at least one team member flying into a murderous rage. This only really happens if your program is a mess and there's sticky goo floating all around the place.
So, what are these usage patterns I've been blubbering about?
One pattern is the (observationally equivalent) immutable value. If your global is a constant, or behaves like a constant for all means and purposes, it makes refactoring a lot easier, because it means that two distinct places in your code cannot communicate through that variable. Of course, it's still possible that the two places expect to receive the same constant (such as an array size), but you've eliminated a whole slew of potential issues. In short, if you can use a constant, do it. If data can be changed, but you can make it look like it never does from 99% of your code, do it and document with appropriate language features (const
Another pattern is the layered architecture. Make every piece of your code part of a layer, and then decide which layers are allowed to do what. MVC is a good example of this: the view cannot change the model data, and the model data cannot interact with the renderer. So, if the renderer was a global, and you needed to un-globalize it, you wouldn't need to worry about the model. As you write your game, you will end up with a lot more layers than just MVC, until it looks like a stack of thin pancakes such as "enemy controllers", "weapon views", "AI models" ... make sure you decide on coherent rules for every such layer as far as global access goes, including rules about when two elements may expect to be using the same instance or not.
Yet another pattern is the thousand faces instance—using polymorphism or encapsulation, hide a single instance behind several types that represent different aspects of what it can do. Even though right now a "Physics Simulator" and an "AI Physics Simulator" are the exact same thing (down to the method names and types), you can have one type evolve independently of the other and immediately track down where in your code it was used. Once you need to remove the global aspect, you can do so by applying rules on a per-type basis.
Ultimately, when a variable ceases to be global, you need to pass it down to every place that needs it, by making it a member variable of the objects that use it or an argument of the functions that use it (use whichever is appropriate on a case-by-case basis). Don't think that because an instance stops being a global variable, and becomes a member variable of a global variable, it's solved. Global is a different thing from "global variable". If you can access your object without passing it as an argument or creating it yourself, it's global even if it's not a global variable.
The bottom line is: are you comfortable with backtracking on your assumption ten days from now? Ten months from now? Do you have a documented contingency plan? If you're scared about it, backtrack now before it's too late.
I sheathe my sword in the reddish light of a bloodied twilight, neither proud nor ashamed of what had to be done. To my left, Tezuma the Ever-Vigilant runs a few unit tests, going through the motions without really expecting any to fail. Sitting on a moss-covered rock nearby, Matsuru treats her wounds—a stray fang from the beast's death throes, envenomed with the bitter ichor of static initialization order, found its way through her statically typed armor.