什么时候不应该使用单例模式? (除了显而易见的)

发布于 2024-09-29 20:10:25 字数 402 浏览 5 评论 0原文

我很清楚您希望使用 Singleton 来提供对某些状态或服务的全局访问点。单例模式的好处不需要在这个问题中一一列举。

我感兴趣的是 Singleton 起初看起来是一个不错的选择,但可能会反过来咬你的情况。我一次又一次地看到 SO 的书籍和海报中的作者说单例模式通常是一个非常糟糕的主意。

四人帮指出,在以下情况下您需要使用 Singleton:

  • 类必须只有一个实例,并且客户端必须可以从众所周知的访问点访问该实例。
  • 当唯一的实例应该可以通过子类化进行扩展时,并且客户端应该能够使用扩展的实例而不修改其代码。

这些要点虽然确实值得注意,但并不是我所寻求的实际要点。

有没有人有一套规则或警告,可以用来评估您是否真的确定要使用单例?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

墨落成白 2024-10-06 20:10:25

摘要版本:

您知道您使用全局变量的频率吗?好吧,现在更少使用单例了。事实上要少得多。几乎从来没有。它们与全局变量存在隐藏耦合的所有问题(直接影响可测试性和可维护性),并且通常“只能存在一个”限制实际上是一个错误的假设。

详细答案:

关于单例,要认识到的最重要的一点是它是全局状态。它是一种公开全局无限制访问的单个实例的模式。这具有全局变量在编程中存在的所有问题,但也采用了一些有趣的新实现细节,并且在其他方​​面几乎没有真正的价值(或者,实际上,它可能会在单实例方面带来不必要的额外成本)。其实现非常不同,以至于人们经常将其误认为是面向对象的封装方法,而实际上它只是一个奇特的单全局实例。

您应该考虑单例的唯一情况是,拥有多个全局数据实例实际上会导致逻辑或硬件访问错误。即使如此,您通常也不应该直接处理单例,而是提供一个包装器接口,该接口允许根据需要多次实例化,但仅访问全局状态。通过这种方式,您可以继续使用依赖注入如果您能够将全局状态与类的行为分开,那么这并不是整个系统的彻底改变。

然而,当您看似不依赖全球数据,但实际上却依赖全球数据时,就会出现一些微妙的问题。这样(使用接口的依赖注入包装单例)只是一个建议,而不是一个规则。一般来说,它仍然更好,因为至少您可以看到该类依赖于单例,而仅在类成员函数内部使用 ::instance() 函数隐藏了该依赖关系。它还允许您根据全局状态提取类并进行更好的单元测试< /strong> 对于他们来说,你可以传入模拟的无所事事的对象,如果你将单例的依赖直接烘焙到类中,这会困难得多。

当烘焙单例 ::instance 调用时,它也会将自身实例化为类,从而使继承变得不可能。解决方法通常会破坏单例的“单实例”部分。考虑这样一种情况,您有多个项目依赖于 NetworkManager 类中的共享代码。即使您希望此 NetworkManager 处于全局状态和单个实例,您也应该对将其变成单例持怀疑态度。通过创建一个实例化自身的简单单例,基本上使任何其他项目都无法从该类派生。

许多人认为 ServiceLocator 是一种反模式,但我相信它比Singleton 并有效地掩盖了 Go4 模式的目的。实现服务定位器的方法有很多种,但基本概念是将对象的构造和对象的访问分为两个步骤。通过这种方式,在运行时,您可以连接适当的派生服务,然后从单个全局联系点访问它。这具有显式对象构造顺序的优点,并且还允许您从基础对象派生。对于大多数所述的原因,这仍然很糟糕,但它比 Singleton 那么糟糕,并且是一个直接的替代品。

可接受的单例(读:servicelocator)的一个具体示例可能是包装单实例 C 风格接口,如 SDL_mixer。单例的一个例子通常天真地实现在不应该出现的地方,那就是在日志记录类中(当你想记录到控制台和磁盘时会发生什么?或者如果你想单独记录子系统。)

依赖的最重要的问题然而,在全局状态上,当您尝试实施适当的单元测试<时,几乎总是会出现/a> (你应该尝试这样做)。当您无法真正访问的类的内部试图进行彻底的磁盘写入和读取、连接到实时服务器并发送真实数据或从扬声器中发出声音时,处理您的应用程序就会变得更加困难无论如何。使用依赖注入要好得多,这样您就可以在测试计划的情况下模拟一个什么都不做的类(并且看到您需要在类构造函数中执行此操作)并将其指向该计划,而无需预测所有内容你的类所依赖的全局状态。

相关链接:

<模式使用与出现

模式作为想法和术语很有用,但不幸的是,当真正的模式是根据需要实现时,人们似乎觉得有必要“使用”模式。通常,单例被特别硬塞进去只是因为它是一种经常讨论的模式。在设计系统时要充分了解模式,但不要仅仅因为模式的存在而专门设计系统来遵循它们。它们是有用的概念工具,但正如您不会仅仅因为可以使用工具箱中的每个工具一样,您也不应该对模式做同样的事情。根据需要使用它们,不多也不少。

单实例服务定位器示例

#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() << "\n" << ((ServiceDerived::Instance())? ServiceDerived::Instance()->GetData() :-1);
    return 0;
}

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() << "\n" << ((ServiceDerived::Instance())? ServiceDerived::Instance()->GetData() :-1);
    return 0;
}
摘星┃星的人 2024-10-06 20:10:25

一个词:测试

可测试性的标志之一是类的松散耦合,允许您隔离单个类并对其进行完全测试。当一个类使用单例(我说的是经典单例,通过静态 getInstance() 方法强制其自身的奇异性)时,单例用户和单例就不可避免地耦合在一起。如果不测试单例,就不再可能测试用户。

单例测试是一场灾难。因为它们是静态的,所以你不能用子类来消除它们。由于它们是全局的,因此如果不重新编译或进行一些繁重的工作,您就无法轻松更改它们指向的引用。任何使用单例的东西都会神奇地获得对难以控制的东西的全局引用。这使得限制测试范围变得困难。

One word: testing

One of the hallmarks of testability is a loose coupling of classes, allowing you to isolate a single class and test it completely. When one class uses a singleton (and I'm talking about a classic singleton, one that enforces it own singularity thorough a static getInstance() method), the singleton user and the singleton become inextricably coupled together. It is no longer possible to test the user without also testing the singleton.

Singletons are a disaster to test. Since they're static you can't stub them out with a subclass. Since they're global you can't easily change the reference they point to without a recompile or some heavy lifting. Anything that uses the singleton magically gets a global reference to something that's hard to control. This makes it difficult to limit the scope of a test.

翻身的咸鱼 2024-10-06 20:10:25

我见过的 Singleton 最大的错误是你正在设计一个单用户系统(例如,桌面程序)并使用 Singleton 来做很多事情(例如设置),然后你想成为多用户系统,比如网站或服务。

这与具有内部静态缓冲区的 C 函数在多线程程序中使用时所发生的情况类似。

The biggest mistakes with Singleton I've seen are you are designing a single-user system (say, a desktop program) and use Singleton for a lot of things (e.g. Settings) and then you want to become multi-user, like a website or a service.

It's similar to what happened to C functions with internal static buffers when they got used in multi-threaded programs.

月亮邮递员 2024-10-06 20:10:25

我想说的是,不惜一切代价避免单身人士。它限制了应用程序的扩展。真正分析您处理的问题并考虑可扩展性,并根据您希望应用程序的可扩展性做出决策。

归根结底,如果设计不正确,单例就会成为资源瓶颈。

有时,您会在没有完全理解这样做对您的应用程序产生什么影响的情况下引入此瓶颈。

我在处理尝试访问单例资源但陷入死锁的多线程应用程序时遇到了问题。这就是为什么我尽可能避免使用单身人士。

如果您在设计中引入单例,请确保您了解运行时的影响,绘制一些图表并找出可能导致问题的位置。

I would say avoid singletons at all cost. It restricts the scaling of applications. Really analyze the problem your dealing with and think about the scalability and make decisions based on how scalable you want your application.

At the end of the day, a singleton acts as a resource bottleneck if designed incorrectly.

Sometimes you introduce this bottleneck without fully understanding what the implications of doing so will have on your application.

I have come across issues when dealing with multi-threaded applications that are trying to access a singleton resource, but get into deadlocks. This is why I try to avoid a singleton as much as possible.

If you introduce singletons in your design, make sure you understand the runtime implications, do some diagrams and figure out where it could cause an issue.

岁月蹉跎了容颜 2024-10-06 20:10:25

Singleton 通常被用作人们懒得用适当的访问器将其正确封装在实际需要的地方的东西的包罗万象。

最终结果是一个 tarball,它最终收集了整个系统中的所有static。这里有多少人从未在他们必须使用的一些所谓的 OOD 代码中见过名为 Globals 的类?啊。

Singleton is often used as a catch all for stuff that people cannot be bothered to encapsulate properly in the place where it's actually needed, with appropriate accessors.

The end result is a tarball that eventually gathers all statics in the entire system. How many people here have NEVER seen a class called Globals in some supposedly OOD code they have had to work with? Ugh.

妖妓 2024-10-06 20:10:25

我不会在任何设计中完全避免它。然而,人们必须小心它的使用。在许多情况下,它可能成为上帝的对象,从而违背目的。

请记住,这种设计模式是解决某些问题的解决方案,但不是解决所有问题。事实上,对于所有的设计模式来说都是一样的。

I wouldn't avoid it completely in any design. However, one must be careful about its usage. It can become the God object in many scenarios and hence defeat the purpose.

Bear in mind, this design pattern is a solution to solve some problems but not all problems. In fact, it is the same for all the design patterns.

↘紸啶 2024-10-06 20:10:25

我不认为自己是一个经验丰富的程序员,但我目前的观点是,你实际上不需要单例......是的,一开始似乎更容易使用(类似于全局变量),但随后出现的是“哦,天哪” “当一个人需要另一个实例的时刻。

你总是可以传递或注入实例,我真的没有看到使用 Singleton 会更容易或有必要的情况

即使我们拒绝一切,仍然存在代码的可测试性问题

I don't consider myself an experienced programmer, but my current opinion is that you actually don't need the Singleton ... yes, it seems easier to work with at first (similarly to globals), but then comes the "oh my" moment when one needs another instance.

You can always pass or inject the instance, I don't really see a situation where it would be significantly easier or necessary to use Singleton

Even if we reject everything, there's still the question of testability of the code

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文