Once Is Not Enough

Don't let Singletons degenerate into global variables.


March 01, 2003
URL:http://www.drdobbs.com/once-is-not-enough/184401625

Conversations: Once Is Not Enough


I was trying to write unit tests for some code I'd developed. Trying -- that's what the code was, all right. I kept running into brick walls. I suppressed my urge to let out a primeval scream and settled for a nice, quiet sigh.

"Wazzup, pardner?" Wendy's voice floated over the cubicle wall. Either I had sighed louder than I'd thought, or Wendy was developing the same kind of prescience that the Guru seemed to have.

"It's these darn unit tests," I said as I gophered up to talk to her. "I keep having to link in more and more objects just to test the one little piece I'm working on. The class I'm working on uses some Singletons, so I have to link in their object files. Those Singletons use other Singletons, and so on, so now I have this huge, bloated mess. It's getting to the point that I have to instantiate most of the services just to test this one simple object."

"And that, my child, is but one reason to avoid Singletons." I almost fell off my chair as I jumped at the Guru's soft voice behind me.

"Come again?" I said, as I climbed down.

"You have discovered -- or should I say, rediscovered -- one of the reasons for avoiding Singletons: they make your writings difficult to test. Your object depends on the service object to provide specific services and behavior. The simplest way to test your object is to replace the service object with a mock object [1]."

"I know that," I said, trying to keep the smugness out of my voice, "but another team wrote the service and implemented it as a Singleton. I can't go back to them and ask them to rewrite their code, so I had no choice..."

"No choice, say you?" the Guru interrupted. "Always there are choices. Instead of directly calling the Singleton's entry point, your object can accept a pointer or reference to the service object in its initialization or creation functions. Your unit test can simply create a mock service object and pass that to your object to be tested. Consider this parable," the Guru picked up the whiteboard marker and began writing in her fine, spidery script:

class HighlyCoupled
{
  void SomeMemberFn()
  {
    AService & service =
      AService::Instance();
    service.DoSomething();
  }
};
"This class suffers from Singleton abuse. It is tightly coupled to the Singleton, making it difficult to test. Instead, you can keep a reference to the service as a class member:"

class LessCoupled
{
  AService & service_;
  void SomeMemberFn()
  {
    service_.DoSomething();
  }
public:
  LessCoupled(AService & service)
  : service_( service )
  {}
};
"Or, if the service is used by only one or two member functions, pass it as a parameter to the function:"

class EvenLessCoupled
{
   void SomeMemberFn(
     AService & service )
  {
    service.DoSomething();
  }
};
"Now, passing a mock object in your unit test becomes trivial."

"What did you call it... 'Singleton abuse'?" I asked.

"My child, the Singleton design pattern is one of the most inappropriately used patterns. Singletons are intended to be used when a class must have exactly one instance, no more, no less [2]. In many cases where scribes use Singletons, the fact that it is a 'single instance' just happens to be a coincidence. The application could happily instantiate multiple instances with no problems.

"Scribes frequently use Singletons in a misguided attempt to replace global variables. I have, for example, been on projects where the Singleton has been described as 'a well-known object' -- does that not sound like a global variable, my child?"

"Umm... no, not really," I drawled after a bit of consideration. I wrote on my whiteboard:

S &S::Instance()
{
  static S theInstance;
  return theInstance;
}
"I don't see a global variable here."

"Ah, my child, but there is a global variable -- a global variable named S::Instance(). Consider this parable:

class T { /* whatever */ };
T globalT;
"What is the difference between globalT and theInstance? Both variables have static storage duration, do they not?"

"But!" I cried. "globalT has global scope, whereas theInstance's scope is limited to the member function. Therefore the Singleton is not a global variable."

"By the letter of the Holy Standard, yes, its scope is limited to the member function. Its lifetime, however, is not. Meditate upon these two allegories:"

globalT.DoSomething();
S::Instance().DoSomething();
"What is the difference, in terms of how they are used, between globalT and S::Instance()?"

The Guru paused while the gears churned in my brain. I wasn't convinced yet.

"Allow me to take this allegory one step further," she continued. "Recall that a reference is an alias for an existing object:"

T globalT = someInitFunction();
S & globalS = S::Instance();
globalS.DoSomething();
globalT.DoSomething();
"The similarity becomes clearer, does it not? A Singleton is, for all intents and purposes, a global variable. Therefore, never create a Singleton whose sole purpose is to mask a global variable. The Singleton does not do away with the global; it merely renames it. The Singleton clarifies nothing and forces extra typing, causing some scribes to exceed the 34 character limit for code samples imposed by certain print magazines."

"Hold on, back the truck up a second," inspiration finally hit me. "There's another difference here: initialization order. You don't know when globalT will be initialized, relative to global objects in other translation units, but you do know when theInstance will be initialized. So you can use the Singleton pattern to smooth over static initialization order problems." I scribbled on the whiteboard:

extern T globalT;
U notSoGood( globalT );
U muchBetter( T::Instance() );
"If the translation unit with notSoGood gets initialized before the translation unit with globalT, then you could be in big trouble. But with a Singleton, that problem goes away -- muchBetter can rely on the Singleton being initialized first."

"That is true, my child," the Guru pushed back a graying lock of hair, "but yours is not an argument in favor of Singletons, but rather an argument against global variables."

"The Singleton is a deceptively complex concept. On the surface, it seems quite simple. However, crafting a Singleton that initializes properly, is thread safe, and can be destroyed safely is not a trivial matter. Indeed, the prophet Vlissides has preached on the theme of destroying a Singleton [3]."

"So," I soed, "are you proposing that Singleton should be considered harmful?"

"No, my child," the Guru said softly, "not in the least. The problem is not with Singletons, but with the rampant overzealous application of the Singleton pattern where not appropriate. When your only tool is a hammer..."

"Everything looks like a nail," I finished as the Guru turned and silently glided away.

Notes

[1] See also <http://c2.com/cgi/wiki?MockObject>.

[2] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).

[3] J. Vlissides. "To Kill A Singleton," C++ Report, June 1996.

About the Authors

Herb Sutter (<www.gotw.ca>) is convener of the ISO C++ standards committee, author of the acclaimed books Exceptional C++ and More Exceptional C++, and one of the instructors of The C++ Seminar (<www.gotw.ca/cpp_seminar>). In addition to his independent writing and consulting, he is also C++ community liaison for Microsoft.

Jim Hyslop is a senior software designer with over 10 years programming experience in C and C++. Jim works at Leitch Technology International Inc., where he deals with a variety of applications, ranging from embedded applications to Windows programs. He can be reached at [email protected].


Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.