Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
374 views
in Technique[技术] by (71.8m points)

language agnostic - When should the Singleton pattern NOT be used? (Besides the obvious)

I know well that you want to use Singleton to provide a global point of access to some state or service. The benefits of the Singleton pattern do not need to be enumerated in this question.

What I am interested in are the situations when Singleton might seem like a good choice at first, but might come back to bite you. Time and time again, I've seen authors in books and posters on SO say that the Singleton pattern is often a very bad idea.

The Gang of Four states that you'll want to use Singleton when:

  • there must be exactly one instance of a class, and it must be accessible to clients from a well-known access point.
  • when the sole instance should be extensible by subclassing, and clients should be able to use an extended instance without modifying their code.

These points, while certainly notable, are not the practical ones which I seek.

Does anyone have a set of rules or caveats that you use to assess whether you're really, really sure you want to use a Singleton?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Summary Version:

You know how often you use globals? Ok, now use Singletons EVEN LESS. Much less in fact. Almost never. They share all the problems globals have with hidden coupling (directly impacting testability and maintainability), and often the "only one can exist" restriction is actually a mistaken assumption.

Detailed Answer:

The most important thing to realize about a singleton is that it is global state. It is a pattern for exposing a single instance of globally unmitigated access. This has all of the problems in programming which globals have, but also adopts some interesting new implementation details and otherwise very little real value (or, indeed, it may come at an unnecessary extra cost with the single instance aspect). The implementation is different enough that people often mistake it for an object oriented encapsulation method when it is really just a fancy single instance global.

The only situation in which you should consider a singleton is when having more than one instance of already global data would actually be a logical or hardware access error. Even then you should typically not deal with the singleton directly, but instead provide a wrapper interface which is allowed to be instantiated as many times as you need it to be, but only accesses global state. In this manner you can continue to use dependency injection and if you can ever unmarry global state from the behavior of the class it isn't a sweeping change across your system.

There are subtle issues with this, however, when it appears as if you are not relying on global data, but you are. So that (using dependency injection of the interface which wraps the singleton) is only a suggestion and not a rule. In general it is still better because at least you can see that the class relies upon the singleton whereas just using the ::instance() function inside the belly of a class member function hides that dependency. It also allows you to extract classes relying on the global state and make better unit tests for them, and you can pass in mock do-nothing objects where if you bake reliance on the singleton directly into the class this is MUCH more difficult.

When baking a singleton ::instance call which also instantiates itself into a class you make inheritance impossible. Work-arounds typically break the "single instance" part of a singleton. Consider a situation where you have multiple projects relying on shared code in a NetworkManager class. Even if you want this NetworkManager to be global state, and single instance, you should be very skeptical about making it into a singleton. By creating a simple singleton which instantiates itself you are basically making it impossible for any other project to derive from that class.

Many consider the ServiceLocator to be an anti-pattern, however I believe it is a half step better than the Singleton and effectively eclipses the purpose of the Go4 pattern. There are many ways to implement a service locator, but the basic concept is that you break up the construction of the object and the access of the object into two steps. In this way, at runtime, you can connect the appropriate derived service, and then access it from a single global point of contact. This has the benefit of an explicit object construction order, and also allows you to derive from your base object. This is still bad for most of the stated reasons, but it is less bad than the Singleton and is a drop-in replacement.

One specific example of an acceptable singleton(read: servicelocator) may be in wrapping a single-instance c style interface like SDL_mixer. One example of a singleton often naively implemented where it probably shouldn't be is in a logging class (what happens when you want to log to console AND to disk? Or if you want to log subsystems separately.)

The most important problems of relying on global state, however, pretty much always come up when you're trying to implement proper unit testing (and you should be trying to do that). It becomes so much harder to deal with your application when the bowels of classes that you don't really have access to are trying to do unmitigated disk writing and reading, connect to live servers and send real data, or blast sound out of your speakers willy nilly. It's much, MUCH, better to use dependency injection so you can mock up a do-nothing class (and see that you need to do that in the class constructor) in case of a test plan and point it at that without having to divine all the global state your class depends on.

Related Links:

Pattern Use vs Emergence

Patterns are useful as ideas and terms, but unfortunately people seem to feel the need to "use" a pattern when really patterns are implemented as need dictates. Often the singleton specifically is shoehorned in simply because it's a commonly discussed pattern. Design your system with an awareness of patterns, but do not design your system specifically to bend to them just because they exist. They are useful conceptual tools, but just as you don't use every tool in the toolbox just because you can, you shouldn't do the same with patterns. Use them as needed and no more or less.

Example Single-Instance Service Locator

#include <iostream>
#include <assert.h>

class Service {
public:
    static Service* Instance(){
        return _instance;
    }
    static Service* Connect(){
        assert(_instance == nullptr);
        _instance = new Service();
    }
    virtual ~Service(){}

    int GetData() const{
        return i;
    }
protected:
    Service(){}
    static Service* _instance;
    int i = 0;
};

class ServiceDerived : public Service {
public:
    static ServiceDerived* Instance(){
        return dynamic_cast<ServiceDerived*>(_instance);
    }
    static ServiceDerived* Connect(){
        assert(_instance == nullptr);
        _instance = new ServiceDerived();
    }
protected:
    ServiceDerived(){i = 10;}
};

Service* Service::_instance = nullptr;

int main() {
    //Swap which is Connected to test it out.
    Service::Connect();
    //ServiceDerived::Connect();
    std::cout << Service::Instance()->GetData() << "
" << ((ServiceDerived::Instance())? ServiceDerived::Instance()->GetData() :-1);
    return 0;
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...