This chapter is an anomaly. Every other chapter in this book shows you how to use a design pattern. This chapter shows you how not to use one.
这节有点反常。其他章节都是告诉你如何使用一个模式。本节却是告诉你如何不使用一个模式。
Despite noble intentions, the Singleton pattern described by the Gang of Four usually does more harm than good. They stress that the pattern should be used sparingly, but that message was often lost in translation to the game industry.
尽管一再告诫,在四人帮的单件模式描述中,它通常缺点大于优点。他们一再强调这个模式应当谨慎的使用,但是当应用在游戏行业时,这个强调通常被忽略了。
Like any pattern, using Singleton where it doesn't belong is about as helpful as treating a bullet wound with a splint. Since it's so overused, most of this chapter will be about avoiding singletons, but first, let's go over the pattern itself.
和其他模式一样,在不合适的地方使用单件模式,就像用夹板来治疗枪伤一样毫无用处。既然它已经被过度使用了,本节的大部分内容都是关于避免使用单件。不过首先,我们来看看模式本身。
When much of the industry moved to object-oriented programming from C, one problem they ran into was "how do I get an instance?" They had some method they wanted to call but didn't have an instance of the object that provides that method in hand. Singletons (in other words, making it global) were an easy way out.
自从工业界大部分从C转向面向对象编程之后,一个摆在面前的问题就是“如何得到一个实例?”,他们有一些想要调用的方法,但是手上却没有这个对象的实例。单件(或者,使之全局化)是最简单的解决方法。
Design Patterns summarizes Singleton like this:
设计模式这样总结单件:
Ensure a class has one instance, and provide a global point of access to it.
确保一个类只有一个实例,并提供一个全局的指针访问它。
We'll split that at "and" and consider each half separately.
我们将分别讨论“并”前后的两点。
There are times when a class cannot perform correctly if there is more than one instance of it. The common case is when the class interacts with an external system that maintains its own global state.
在有些情况下,一个类如果有多个实例就不能正常运作。最常见的情况就是这个类和一些关联全局状态的额外类进行交互。
Consider a class that wraps an underlying file system API. Because file operations can take a while to complete, our class performs operations asynchronously. This means multiple operations can be running concurrently, so they must be coordinated with each other. If we start one call to create a file and another one to delete that same file, our wrapper needs to be aware of both to make sure they don't interfere with each other.
比如说一个封装了底层文件API的类。因为文件操作需要一定时间去完成,我们的类将异步地处理。这意味着许多操作可以同时进行,所以他们必须相互协调。如果我们一方面创建文件,一方面去删除这个文件,我们的封装类就必须全部感知,并确保他们不会相互干扰。
To do this, a call into our wrapper needs to have access to every previous operation. If users could freely create instances of our class, one instance would have no way of knowing about operations that other instances started. Enter the singleton. It provides a way for a class to ensure at compile time that there is only a single instance of the class.
为了做到这点,对封装类的调用必须能够访问之前的操作。如果使用者能够自由的创建这个类的实例,一个实例就不能够知道其他实例所做的操作。在单件模式中,他提供了一个编译期能确保某个类只有一个实例的方法。
Several different systems in the game will use our file system wrapper: logging, content loading, game state saving, etc. If those systems can't create their own instances of our file system wrapper, how can they get ahold of one?
游戏中一些其他系统需要用到我们的文件系统封装:日志、文件加载、游戏保存等等。如果这些系统不能够创建他们各自的文件封装实例,他们如果去得到一个呢?
Singleton provides a solution to this too. In addition to creating the single instance, it also provides a globally available method to get it. This way, anyone anywhere can get their paws on our blessed instance. All together, the classic implementation looks like this:
单件提供了一个解决方法。除了创建一个单独的实例外,他还提供一个全局的方法去得到这个实例。这样,就能在其他任何地方都能够等到这个实例了。总体说来,这个类的实现起来像如下这个样子:
class FileSystem
{
public:
static FileSystem& instance()
{
// Lazy initialize.
if (instance_ == NULL) instance_ = new FileSystem();
return *instance_;
}
private:
FileSystem() {}
static FileSystem* instance_;
};
The static instance_
member holds an instance of the class, and the private
constructor ensures that it is the only one. The public static instance()
method grants access to the instance from anywhere in the codebase. It is also
responsible for instantiating the singleton instance lazily the first time
someone asks for it.
instance_
这个静态成员保存这这个类的一个实例,私有的构造函数确保他是唯一的一个实例。静态函数instance()
提供了一个方法能在其他地方得到这个实例。它也负责在第一次访问的时候初始化这个实例,这也叫延时创建。
A modern take looks like this:
实现起来如下:
class FileSystem
{
public:
static FileSystem& instance()
{
static FileSystem *instance = new FileSystem();
return *instance;
}
private:
FileSystem() {}
};
C++11 mandates that the initializer for a local static variable is only run once, even in the presence of concurrency. So, assuming you've got a modern C++ compiler, this code is thread-safe where the first example is not.
C++11 初始化一个局部静态变量时只会运行一次,哪怕是在多线程的情况下也是一样。所以,如果你有一个现代C++编译器的话,下面的代码是线程安全的,而上面的例子却不是:
Of course, the thread-safety of your singleton class itself is an entirely different question! This just ensures that its initialization is.
当然,你的单件类本身的线程安全行完全是另外一个问题!这只是确保他的初始化是线程安全的。
It seems we have a winner. Our file system wrapper is available wherever we need it without the tedium of passing it around everywhere. The class itself cleverly ensures we won't make a mess of things by instantiating a couple of instances. It's got some other nice features too:
看起来我们取得了成效。我们的文件封装能够在任何地方使用而不必将它传递的到处都是。这个类本身机智的保证了我们不会初始化多个实例而将事情弄糟。它还具有一些额外的优良特性。
-
It doesn't create the instance if no one uses it. Saving memory and CPU cycles is always good. Since the singleton is initialized only when it's first accessed, it won't be instantiated at all if the game never asks for it.
-
如果我们不使用,就不会创建实例 节省内存和CPU周期始终是好的。既然单件只在第一次访问的时 候初始化,如果我们游戏始终不使用就不会初始化。
-
It's initialized at runtime. A common alternative to Singleton is a class with static member variables. I like simple solutions, so I use static classes instead of singletons when possible, but there's one limitation static members have: automatic initialization. The compiler initializes statics before
main()
is called. This means they can't use information known only once the program is up and running (for example, configuration loaded from a file). It also means they can't reliably depend on each other -- the compiler does not guarantee the order in which statics are initialized relative to each other. -
他在运行期初始化一个单件的变种是包含多个静态成员的类。我喜欢简单的解决方案,所以我多会使 用静态类而不是单件。但是静态类有一个缺点:自动初始化。编译器早在
main()
函数调用之前就初始 化静态成员了。这以为着他们不能使用只有游戏运行起来才能知道的信息(比如,文件配置)。它还意味 着他们之间不能相互依赖——编译器不能保证他们之间的初始化的顺序。Lazy initialization solves both of those problems. The singleton will be initialized as late as possible, so by that time any information it needs should be available. As long as they don't have circular dependencies, one singleton can even refer to another when initializing itself.
延时初始化解决了以上所有问题。单件会尽可能的延时创建,所以他们需要的信息都是可以得到的。只要 不是循环依赖,一个单件在初始化的时候可以依赖另外一个单件。
-
You can subclass the singleton. This is a powerful but often overlooked capability. Let's say we need our file system wrapper to be cross-platform. To make this work, we want it to be an abstract interface for a file system with subclasses that implement the interface for each platform. Here is the base class:
-
你可以继承单件 这是一个强大但是过多使用的能力。假设我们需要我们的文件封装跨平台。为了实 现这一点,我们将它实现为一个抽象接口,他的子类提供各个平台上的实现。下面是基本的结构:
class FileSystem { public: virtual ~FileSystem() {} virtual char* readFile(char* path) = 0; virtual void writeFile(char* path, char* contents) = 0; };
Then we define derived classes for a couple of platforms: 之后,我们为不同平台定义派生类:
class PS3FileSystem : public FileSystem { public: virtual char* readFile(char* path) { // Use Sony file IO API... } virtual void writeFile(char* path, char* contents) { // Use sony file IO API... } }; class WiiFileSystem : public FileSystem { public: virtual char* readFile(char* path) { // Use Nintendo file IO API... } virtual void writeFile(char* path, char* contents) { // Use Nintendo file IO API... } };
Next, we turn
FileSystem
into a singleton:接下来,我们将
FileSystem
变为一个单件:class FileSystem { public: static FileSystem& instance(); virtual ~FileSystem() {} virtual char* readFile(char* path) = 0; virtual void writeFile(char* path, char* contents) = 0; protected: FileSystem() {} };
The clever part is how the instance is created:
机智之处是如何创建实例的:
FileSystem& FileSystem::instance() { #if PLATFORM == PLAYSTATION3 static FileSystem *instance = new PS3FileSystem(); #elif PLATFORM == WII static FileSystem *instance = new WiiFileSystem(); #endif return *instance; }
With a simple compiler switch, we bind our file system wrapper to the appropriate concrete type. Our entire codebase can access the file system using
FileSystem::instance()
without being coupled to any platform-specific code. That coupling is instead encapsulated within the implementation file for theFileSystem
class itself.随着一个简单的编译跳转,我们将文件封装编译到正确的系统上。我们整个代码可以通过
FileSystem::instance()
来访问文件系统,而不必加上任何平台相关的代码。关联的代码封装在实现FileSystem
这个类的文件之中了。
This takes us about as far as most of us go when it comes to solving a problem like this. We've got a file system wrapper. It works reliably. It's available globally so every place that needs it can get to it. It's time to check in the code and celebrate with a tasty beverage.
它花费了我们我们之中绝大数人解决这类问题所花费的时间(译注:绝大部分人解决这类问题到此为止)。我们得到了一个文件封装。他工作的很好,它全局可用,每处需要使用的地方都能访问它。是时候提交代码,来点美味的饮料庆祝了。
In the short term, the Singleton pattern is relatively benign. Like many design choices, we pay the cost in the long term. Once we've cast a few unnecessary singletons into cold hard code, here's the trouble we've bought ourselves:
在短期内,单件模式是相对温和的。像其他一些设计取舍一样,我们会在长时间内付出代价。一旦我们将一些不必要的单件扔到了冰硬的代码之中,我们就为自己带来了一系列的麻烦。
When games were still written by a couple of guys in a garage, pushing the hardware was more important than ivory-tower software engineering principles. Old-school C and assembly coders used globals and statics without any trouble and shipped good games. As games got bigger and more complex, architecture and maintainability started to become the bottleneck. We struggled to ship games not because of hardware limitations, but because of productivity limitations.
当游戏还是车库里的借个家伙写的时候,硬件要比软件工程准则更为总要。随着游戏变得更大更复,架构和开始变为短板。我们挣扎这放弃游戏不是应为硬件限制,而是因为开发效率
So we moved to languages like C++ and started applying some of the hard-earned wisdom of our software engineer forebears. One lesson we learned is that global variables are bad for a variety of reasons:
所以我们开始学习C++这样的语言,并且应用我们软件开发前驱总结的智慧。我们学到的一个教训就是,全局变量是有害的。理由如下:
-
They make it harder to reason about code. Say we're tracking down a bug in a function someone else wrote. If that function doesn't touch any global state, we can wrap our heads around it just by understanding the body of the function and the arguments being passed to it.
-
他们导致能更难理解的代码 假设我们正在跟踪一个bug。如果这个函数不使用全局状态,我们可以 将精力集中起来,只要理解他的函数体就可以了,和传递给他的参数就可以了。
Computer scientists call functions that don't access or modify global state "pure" functions. Pure functions are easier to reason about, easier for the compiler to optimize, and let you do neat things like memoization where you cache and reuse the results from previous calls to the function.
计算机科学家称不访问或者不修改全局状态的函数为“纯函数”。纯函数易于理解,利于编译器优化。
While there are challenges to using purity exclusively, the benefits are enticing enough that computer scientists have created languages like Haskell that only allow pure functions. 因为全部使用纯函数有不少挑战,但是足够诱使计算机科学家发明Haskell这样只允许存函数的语言。
Now, imagine right in the middle of that function is a call to
SomeClass::getSomeGlobalData()
. To figure out what's going on, we have to hunt through the entire codebase to see what touches that global data. You don't really hate global state until you've had togrep
a million lines of code at three in the morning trying to find the one errant call that's setting a static variable to the wrong value.现在,让我们来看这个函数中间的
SomeClass::getSomeGlobalData()
这个调用。为了搞清楚其中发生了什么,我们需要查看整个代码库来看是谁访问了全局状态。在你不得不大清晨grep
百万行代码来找出究竟是那一个错误的调用将一个静态变量设置错了之前,你是不会真正痛恨全局状态的。 -
They encourage coupling. The new coder on your team isn't familiar with your game's beautifully maintainable loosely coupled architecture, but he's just been given his first task: make boulders play sounds when they crash onto the ground. You and I know we don't want the physics code to be coupled to audio of all things, but he's just trying to get his task done. Unfortunately for us, the instance of our
AudioPlayer
is globally visible. So, one little#include
later, and our new guy has compromised a carefully constructed architecture. -
这了促进了耦合。 你团队的开发新手不熟悉游戏优美的松耦合架构,但是他却有了第一项任务:让石头撞在地上时发出声音。你我都知道,我们不想让物理引擎代码和音频代码耦合起来,但是新手只是一心想完成任务。不幸的是,我们的
AudioPlayer
这个类实例是全局可见的。所以,在一小段#include
之后,我们的新伙伴搞乱了一个仔细构建的架构。Without a global instance of the audio player, even if he did
#include
the header, he still wouldn't be able to do anything with it. That difficulty sends a clear message to him that those two modules should not know about each other and that he needs to find another way to solve his problem. By controlling access to instances, you control coupling.如果没有音频播放器的全局实例,即使他确实
#include
头文件,他也不能做任何事情。这个困难度给他传递了一个明确的消息,这两个模块不应该相互了解,他应该找另外的方式去解决这个问题。通过控制实例的访问,你控制了耦合。 -
They aren't concurrency-friendly. The days of games running on a simple single-core CPU are pretty much over. Code today must at the very least work in a multi-threaded way even if it doesn't take full advantage of concurrency. When we make something global, we've created a chunk of memory that every thread can see and poke at, whether or not they know what other threads are doing to it. That path leads to deadlocks, race conditions, and other hell-to-fix thread-synchronization bugs.
-
它对并发不友好现在在单核上运行游戏的日子已经很远了。即使他们有利用到并发的全部优势。当我们设置为全局时,我们创建了一段内存,每个线程都能够查看和修改它,不管他们时候知道其他线程正在操作它。这有可能导致死锁,条件竞争,和其他一些难以修复的线程同步的Bug。
Issues like these are enough to scare us away from declaring a global variable, and thus the Singleton pattern too, but that still doesn't tell us how we should design the game. How do you architect a game without global state?
上面这些问题足够吓退我们去声明一个全局变量了,同样也适用于单件模式,但是现在还是没有告诉我们该如何设计游戏。在没有全局状态的情况下,该如何构建游戏呢?
There are some extensive answers to that question (most of this book in many ways is an answer to just that), but they aren't apparent or easy to come by. In the meantime, we have to get games out the door. The Singleton pattern looks like a panacea. It's in a book on object-oriented design patterns, so it must be architecturally sound, right? And it lets us design software the way we have been doing for years.
这个问题有几个拓展的答案(本书的绝大部分从某些方面来说就是这个),但是他们不是和明显或者简单能够得到。 与此同时,我们需要发布我们的游戏。单件模式就像一帖万能药。他在一本关于面向对象设计模式中,所以它肯定是架构合理的,对吧?并且他能像之前我们开发了N年那样去设计软件。
Unfortunately, it's more placebo than cure. If you scan the list of problems that globals cause, you'll notice that the Singleton pattern doesn't solve any of them. That's because a singleton is global state -- it's just encapsulated in a class.
不幸的是,它更多的是一种宽慰而不是解决方法。如果你浏览一边全局对象造成的问题,你会注意到单件模式没有解决任何一个。这是因为,一个单件就是全局状态——他只是被封装到了一个类中而已。
The word "and" in the Gang of Four's description of Singleton is a bit strange. Is this pattern a solution to one problem or two? What if we have only one of those? Ensuring a single instance is useful, but who says we want to let everyone poke at it? Likewise, global access is convenient, but that's true even for a class that allows multiple instances.
在四人帮的单件模式中那个“和”这个词有点奇怪。这个模式解决的是一个问题还是两个问题?如果我们只用其中的一个问题怎么办?确保一个实例是很有用的,但是谁说我们需要所有的东西都像这样?就好比,全局访问是很方便,但是允许有多个实例却是很常见的。
The latter of those two problems, convenient access, is almost always why we
turn to the Singleton pattern. Consider a logging class. Most modules in the
game can benefit from being able to log diagnostic information. However, passing
an instance of our Log
class to every single function clutters the method
signature and distracts from the intent of the code.
这两个问题的后者,便利的访问,是我们使用单件模式的主要原因。比如一个日志类。许多游戏中的模块都能够从日子模块中获得好处,但是,
The obvious fix is to make our Log
class a singleton. Every function can then
go straight to the class itself to get an instance. But when we do that, we
inadvertently acquire a strange little restriction. All of a sudden, we can no
longer create more than one logger.
很显然,修正这点就是让我们的Log
变为一个单件。每个函数都能直接通过这个类得到这个类的实例。但是当我们这样做是,我们奇怪的得到了一个限制。突然的,我们不能够创建更多的日志器了。
At first, this isn't a problem. We're writing only a single log file, so we only need one instance anyway. Then, deep in the development cycle, we run into trouble. Everyone on the team has been using the logger for their own diagnostics, and the log file has become a massive dumping ground. Programmers have to wade through pages of text just to find the one entry they care about.
起初,这并不是一个问题,我们只写一个日志文件,所以我们只需要一个日志实例。之后,随着开发的深入,我们陷入了麻烦。团队的每个人都使用这个日志器来记录他们自己的日志。
We'd like to fix this by partitioning the logging into multiple files. To do
this, we'll have separate loggers for different game domains: online, UI, audio, gameplay. But we can't. Not only
does our Log
class no longer allow us to create multiple instances, that
design limitation is entrenched in every single call site that uses it:
我们可以通过将日子分割为不同的文件来修正。我们将日志分为不同的游戏区域:在线、界面、音频、游戏。但是我们不能够。不仅仅是应为我们的Log
类不允许我们创建多个实例,还有这个模式的每个单次调用都是像如下这样使用的。
Log::instance().write("Some event.");
In order to make our Log
class support multiple instantiation (like it
originally did), we'll have to fix both the class itself and every line of code
that mentions it. Our convenient access isn't so convenient anymore.
为了是我们的Log
类能够支持多个初始化(想他原来的那样)。我们需要修改这个类的本身和每处调用这个类的地方。我们便利的访问也不那么便利了。
It could be even worse than this. Imagine your
Log
class is in a library being shared across several games. Now, to change the design, you'll have to coordinate the change across several groups of people, most of whom have neither the time nor the motivation to fix it.
情况也许会比这样更为糟糕。假如你的
Log
内在多个游戏共享的一个库文件中。现在,修改设计,你需要考虑的不同团队的人,他们之中的大部分人都没有时间也没有动机去修改它。
In the desktop PC world of virtual memory and soft performance requirements, lazy initialization is a smart trick. Games are a different animal. Initializing a system can take time: allocating memory, loading resources, etc. If initializing the audio system takes a few hundred milliseconds, we need to control when that's going to happen. If we let it lazy-initialize itself the first time a sound plays, that could be in the middle of an action-packed part of the game, causing visibly dropped frames and stuttering gameplay.
为了满足PC游戏内存和软件效率的需求,延时实例化是一个聪明的技巧。游戏是个不同的怪兽。实例化一个系统需要花费时间:分配内存,加载资源等等。如果实例化音频系统需要花费几百毫秒,我们需要控制住何时实例化。如果我们让他在第一次播放声音的时候延时实例化,这有可能在游戏正酣的时候,导致明显的掉帧和游戏卡顿。
Likewise, games generally need to closely control how memory is laid out in the heap to avoid fragmentation. If our audio system allocates a chunk of heap when it initializes, we want to know when that initialization is going to happen, so that we can control where in the heap that memory will live.
同样的,游戏通常需要仔细的控制内存在堆中的布局来防止分段。如果我们的音频系统在实例化是分配了内存,我们需要知道实例化发生的时间,以便让我们控制它在堆中的内存布局。
See Object Pool for a detailed explanation of memory fragmentation.
查看 对象池 活的内存分段的详细解释。
Because of these two problems, most games I've seen don't rely on lazy initialization. Instead, they implement the Singleton pattern like this:
介于这两点问题,我见过的大部分游戏都不依赖延时初始化。相反,他们想这样实现单件模式。
class FileSystem
{
public:
static FileSystem& instance() { return instance_; }
private:
FileSystem() {}
static FileSystem instance_;
};
That solves the lazy initialization problem, but at the expense of discarding several singleton features that do make it better than a raw global variable. With a static instance, we can no longer use polymorphism, and the class must be constructible at static initialization time. Nor can we free the memory that the instance is using when not needed.
这解决的延时初始化的问题,但是这也丢失了单件比一个全局变量更好的几个特性。随着一个静态实例,我们不能够使用多态了,并且这个类必须能够在静态初始化的时候构造。我们也不能够在不需要这个类的时候释放这段内存。
Instead of creating a singleton, what we really have here is a simple static
class. That isn't necessarily a bad thing, but if a static class is all you
need, why not get rid of the instance()
method
entirely and use static functions instead? Calling Foo::bar()
is simpler than
Foo::instance().bar()
, and also makes it clear that you really are dealing
with static memory.
与创建单件相反,这里我们真正需要的是一个静态类。这不完全是一件坏事,如果你想要的仅仅是静态类,何不消除instance()
这个方法而使用简单函数呢?调用Foo::bar()
要比Foo::instance().bar()
简单不说,还能澄清你正在使用静态内存。
The usual argument for choosing singletons over static classes is that if you decide to change the static class into a non-static one later, you'll need to fix every call site. In theory, you don't have to do that with singletons because you could be passing the instance around and calling it like a normal instance method.
通常关于静态类和单件的争论是,如果之后你决定将一个静态类转变为非静态类,你必须修改没处调用的地方。理论上,对于单件,你可以不必这样做,因为你可以将实例相互传递并且像一个普通实例一样去调用。
In practice, I've never seen it work that way. Everyone just does
Foo::instance().bar()
in one line. If we changed Foo to not be a
singleton, we'd still have to touch every call site. Given that, I'd rather have
a simpler class and a simpler syntax to call into it.
在实践中,我从没有见过这么做过。每个人都是Foo::instance().bar()
这样调用的。如果我们将Foo
改为非单件,我们必须修改每处调用的地方。有鉴于此,我更倾向于使用一个简单的类和一个简单的语法去调用它。
If I've accomplished my goal so far, you'll think twice before you pull Singleton out of your toolbox the next time you have a problem. But you still have a problem that needs solving. What tool should you pull out? Depending on what you're trying to do, I have a few options for you to consider, but first...
如果现在我完成了目标,在下次你遇到问题时,在你祭出单件大发是会多考虑两次。但你还有一个问题有待解决。你需要什么样的工具?这要取决于你想要做什么,我有几个建议可供参考,不过首先...
Many of the singleton classes I see in games are "managers" -- those nebulous classes that exist just to babysit other objects. I've seen codebases where it seems like every class has a manager: Monster, MonsterManager, Particle, ParticleManager, Sound, SoundManager, ManagerManager. Sometimes, for variety, they'll throw a "System" or "Engine" in there, but it's still the same idea.
游戏中的许多单件类都是"managers"——这些保姆类存在就是为了管理其他对象。我见识过一个代码库,里面好像每个类都有一个管理者:Monster, MonsterManager, Particle, ParticleManager, Sound, SoundManager, ManagerManager。有时,为了区别,他们叫做"System'或者"Engine",却是换汤不换药。
While caretaker classes are sometimes useful, often they just reflect unfamiliarity with OOP. Consider these two contrived classes:
尽管保姆类有时是有用的,通常这反应他们对OOP不熟悉。考虑这两个我构造的类:
class Bullet
{
public:
int getX() const { return x_; }
int getY() const { return y_; }
void setX(int x) { x_ = x; }
void setY(int y) { y_ = y; }
private:
int x_, y_;
};
class BulletManager
{
public:
Bullet* create(int x, int y)
{
Bullet* bullet = new Bullet();
bullet->setX(x);
bullet->setY(y);
return bullet;
}
bool isOnScreen(Bullet& bullet)
{
return bullet.getX() >= 0 &&
bullet.getX() < SCREEN_WIDTH &&
bullet.getY() >= 0 &&
bullet.getY() < SCREEN_HEIGHT;
}
void move(Bullet& bullet)
{
bullet.setX(bullet.getX() + 5);
}
};
Maybe this example is a bit dumb, but I've seen plenty of code that reveals a
design just like this after you scrape away the crusty details. If you look at
this code, it's natural to think that BulletManager
should be a singleton. After
all, anything that has a Bullet
will need the manager too, and how many
instances of BulletManager
do you need?
或许这个例子有点愚蠢,如果你查看这段代码,将BulletManager
当作单件是很自然的事情。毕竟,一个Bullet
需要用一个东西来管理,而你需要有多个管理器呢?
The answer here is zero, actually. Here's how we solve the "singleton" problem for our manager class:
答案是零,实际上,我们是这样解决我们管理类的"单件"问题的:
class Bullet
{
public:
Bullet(int x, int y) : x_(x), y_(y) {}
bool isOnScreen()
{
return x_ >= 0 && x_ < SCREEN_WIDTH &&
y_ >= 0 && y_ < SCREEN_HEIGHT;
}
void move() { x_ += 5; }
private:
int x_, y_;
};
There we go. No manager, no problem. Poorly designed singletons are often "helpers" that add functionality to another class. If you can, just move all of that behavior into the class it helps. After all, OOP is about letting objects take care of themselves.
就这样。没有管理器也没有问题。错误的设计单件通常会“帮助”你将功能添加到别的类中。如果可以,你只需将这些功能移动到它帮助的类中去就可以了。比较,面向对象就是让对象自己管理自己。
Outside of managers, though, there are other problems where we'd reach to Singleton for a solution. For each of those problems, there are some alternative solutions to consider.
除了管理器,毕竟,这里还有别的问题我们需要求助单件模式去解决。对于这些问题,这里有一些额外的解决方案可供考虑。
This is one half of what the Singleton pattern gives you. As in our file system example, it can be critical to ensure there's only a single instance of a class. However, that doesn't necessarily mean we also want to provide public, global access to that instance. We may want to restrict access to certain areas of the code or even make it private to a single class. In those cases, providing a public global point of access weakens the architecture.
这是单件模式给你解决的另外一个问题。在我们的文件系统例子中,保证这个类只有一个实例是很有必要的。但是,这不意味这我们也想提供这个实例公共的全局的访问。我们也许想要严格限制在莫一部分代码中,或者干脆将它作为一个类的私有成员。在这种情况下,提供一个全局的指针访问削弱了整体框架。
For example, we may be wrapping our file system wrapper inside another layer of abstraction.
比如,我们可以将我们的文件系统包装在另外一个抽象层中。
We want a way to ensure single instantiation without providing global access. There are a couple of ways to accomplish this. Here's one:
我们提供一种方法来保证单个实例,并且不提供全局访问。这里有几种方法可以达到这点,下面就是一例:
class FileSystem
{
public:
FileSystem()
{
assert(!instantiated_);
instantiated_ = true;
}
~FileSystem() { instantiated_ = false; }
private:
static bool instantiated_;
};
bool FileSystem::instantiated_ = false;
This class allows anyone to construct it, but it will assert and fail if you try to construct more than one instance. As long as the right code creates the instance first, then we've ensured no other code can either get at that instance or create their own. The class ensures the single instantiation requirement it cares about, but it doesn't dictate how the class should be used.
这个类允许所有人创建它,但是如果你想要创建操作一个实例,它会断言并且失败。只要代码创建了第一个实例,我们保证其他代码要么得到这个实例要么创建一个自己的实例。这个类保证了它的单个实例,但是它不能保证这个类如何使用。
An assertion function is a way of embedding a contract into your code. When
assert()
is called, it evaluates the expression passed to it. If it evaluates totrue
, then it does nothing and lets the game continue. If it evaluates tofalse
, it immediately halts the game at that point. In a debug build, it will usually bring up the debugger or at least print out the file and line number where the assertion failed.
一个断言函数就是在我们代码中嵌入一份契约。当
assert()
调用是,他计算传递给它的表达式。当表达式为true
时,它什么都不做,并让游戏继续。当表达式为false
时,它在此处立刻挂断游戏。在一个debug构建中,它通常会启动调试器或者至少将断言失败的文件名和行号打印出来。
An
assert()
means, "I assert that this should always be true. If it's not, that's a bug and I want to stop now so you can fix it." This lets you define contracts between regions of code. If a function asserts that one of its arguments is notNULL
, that says, "The contract between me and the caller is that I will not be passedNULL
."
一个
assert()
意味着,“我确保这个应该始终为true,如果不是,这就是一个bug,并且我想立刻停止以便让我修复它。”这可以让你在代码区间定义约定。如果一个函数断言它的某个参数不为NULL
,也就是说,“函数和调用着之间的契约就是不能够传递NULL
。”
Assertions help us track down bugs as soon as the game does something unexpected, not later when that error finally manifests as something visibly wrong to the user. They are fences in your codebase, corralling bugs so that they can't escape from the code that created them.
断言帮助我们在游戏做一些未预料的事情时离开开始追踪bug,而不是等到错误体现在用户可见的错误上。它们是代码库的围栏,以防bug在发生的代码之处逃离出去。
The downside with this implementation is that the check to prevent multiple instantiation is only done at runtime. The Singleton pattern, in contrast, guarantees a single instance at compile time by the very nature of the class's structure.
这份实现的不足之处在它只在运行期检测来防止多个实例。单件模式,相反的,在编译期就通过类结构自然的保证了单个实例。
Convenient access is the main reason we reach for singletons. They make it easy to get our hands on an object we need to use in a lot of different places. That ease comes at a cost, though -- it becomes equally easy to get our hands on the object in places where we don't want it being used.
便利的访问是我们使用单件额主要原因。它让我们在许多不同地方得到一个对象变得简单。这种便利也有代价,——它也是的我们在不想使用的地方也可以轻松的得到这个对象。
The general rule is that we want variables to be as narrowly scoped as possible while still getting the job done. The smaller the scope an object has, the fewer places we need to keep in our head while we're working with it. Before we take the shotgun approach of a singleton object with global scope, let's consider other ways our codebase can get access to an object:
通用的原则是,在保证功能的情况下将变量限制在一个狭窄的范围内。对象的作用域越小,我们需要用到它的地方就越少。在我们直接了当的通过全局作用域来访问一个单件对象时,让我们考察一下我们代码访问一个对象的其他方式:
-
Pass it in. The simplest solution, and often the best, is to simply pass the object you need as an argument to the functions that need it. It's worth considering before we discard it as too cumbersome.
-
传递进去 最简单,通常也是最好的方法就是简单的将这个对象当作一个参数传递给需要他的函数。
Some use the term "dependency injection" to refer to this. Instead of code reaching out and finding its dependencies by calling into something global, the dependencies are pushed in to the code that needs it through parameters. Others reserve "dependency injection" for more complex ways of providing dependencies to code.
有些人使用术语“依赖注入”来指代这点。与在外部通过调用全局对象来查找依赖不同,依赖通过参数传递到需要的代码“里面”。其他通过储备”依赖注入“来为代码依赖提供更复杂的方式。
Consider a function for rendering objects. In order to render, it needs access to an object that represents the graphics device and maintains the render state. It's very common to simply pass that in to all of the rendering functions, usually as a parameter named something like
context
.假设一个渲染物体的函数。为了渲染,他需要访问这个物体的图形设备的表象并维持渲染状态。简单地将他们全部传递到渲染函数中是很普遍的做法,通常这个参数叫做
context
。On the other hand, some objects don't belong in the signature of a method. For example, a function that handles AI may need to also write to a log file, but logging isn't its core concern. It would be strange to see
Log
show up in its argument list, so for cases like that we'll want to consider other options.另一方面,一个对象不属于某个函数的签名。举个例子,一个操作AI的函数可能也需要写一个日志文件,但是记录日志并不是它主要关系的东西。在它的参数列表中发现有
Log
会很奇怪,所以为了这些情况,我们需要参考其他方法。The term for things like logging that appear scattered throughout a codebase is "cross-cutting concern". Handling cross-cutting concerns gracefully is a continuing architectural challenge, especially in statically typed languages.
描述想日志这种分散的出现在代码库的术语称为”横切关注点“。优雅的处理横切关注点是可持续架构的挑战。尤其是在静态类型语言中。
Aspect-oriented programming was designed to address these concerns. 面向方面程序设计就是设计用来解决这些问题。
-
Get it from the base class. Many game architectures have shallow but wide inheritance hierarchies, often only one level deep. For example, you may have a base
GameObject
class with derived classes for each enemy or object in the game. With architectures like this, a large portion of the game code will live in these "leaf" derived classes. This means that all these classes already have access to the same thing: theirGameObject
base class. We can use that to our advantage: -
在基类中访问它。 许多游戏架构有浅层次但是广泛的继承,通常只有一层继承。举个例子,你可能有一个
GameObject
基类,每个地方或者游戏物体都派生只这个类。有了这样的架构,游戏代码的绝大部分都在资额些派生类的“叶子”上。这意味着所有这些类都能访问同样的东西:他们的GameObject
基类。我们可以利用这点:class GameObject { protected: Log& getLog() { return log_; } private: static Log& log_; }; class Enemy : public GameObject { void doSomething() { getLog().write("I can log!"); } };
This ensures nothing outside of
GameObject
has access to itsLog
object, but every derived entity does usinggetLog()
. This pattern of letting derived objects implement themselves in terms of protected methods provided to them is covered in the Subclass Sandbox chapter.这保证了在
GameObject
之外没有访问Log
对象的代码,但是每个派生类能够通过getLog()
访问。这种让派生类在所提供的保护方法中提供实现的模式在 子类沙盒 章节中讨论.This raises the question, "how does
GameObject
get theLog
instance?" A simple solution is to have the base class simply create and own a static instance.这提出了新的问题。“
GameObject
如何得到Log
实例?”一个简单的方案是,将基类创建出来,并拥有一个自己的实例。If you don't want the base class to take such an active role, you can provide an initialization function to pass it in or use the Service Locator pattern to find it.
如果我们不想让基类承当这个角色,你可以提供一个初始化函数将它传递进去,或者使用 服务定位器模式来得到它。
-
Get it from something already global. The goal of removing all global state is admirable, but rarely practical. Most codebases will still have a couple of globally available objects, such as a single
Game
orWorld
object representing the entire game state. -
通过其他全局对象访问它。 将所有全局状态都移除令人敬佩,但是不够实际。许多代码库仍然有一些全局对象,比如一个单独的
Game
或者World
对象来代表整个游戏状态。We can reduce the number of global classes by piggybacking on existing ones like that. Instead of making singletons out of
Log
,FileSystem
, andAudioPlayer
, do this: 我们能够通过打包到一个已知的全局对象类中来减少全局对象的数量。与将Log
,FileSystem
,和AudioPlayer
变为单件不同:class Game { public: static Game& instance() { return instance_; } // Functions to set log_, et. al. ... Log& getLog() { return *log_; } FileSystem& getFileSystem() { return *fileSystem_; } AudioPlayer& getAudioPlayer() { return *audioPlayer_; } private: static Game instance_; Log *log_; FileSystem *fileSystem_; AudioPlayer *audioPlayer_; };
With this, only
Game
is globally available. Functions can get to the other systems through it:通过这点,只有
Game
是全局可见的。函数能够通过这个来访问其他系统:Game::instance().getAudioPlayer().play(VERY_LOUD_BANG);
Purists will claim this violates the Law of Demeter. I claim that's still better than a giant pile of singletons.
纯粹主义者会声称这违反了迪米特法则。我坚持这仍然要比一对单件要好。
If, later, the architecture is changed to support multiple
Game
instances (perhaps for streaming or testing purposes),Log
,FileSystem
, andAudioPlayer
are all unaffected -- they won't even know the difference. The downside with this, of course, is that more code ends up coupled toGame
itself. If a class just needs to play sound, our example still requires it to know about the world in order to get to the audio player.如果,随后,架构会变得支持多个
Game
实例(也许是为了流或者测试目的),Log
,FileSystem
和AudioPlayer
都不会影响。——他们甚至不知道任何不同。这个副作用,当然,就是更多的代码耦合在了Game
当中。如果一个类只是为了播放声音,我们的例子仍然需要知道全部,以便能够得到声音播放器。We solve this with a hybrid solution. Code that already knows about
Game
can simply accessAudioPlayer
directly from it. For code that doesn't, we provide access toAudioPlayer
using one of the other options described here.我们通过一个混合方案解决这点。如果代码已经知道了
Game
就直接通过它来访问AudioPlayer
。如果代码不知道,我们通过这里讨论的其他方法来访问AudioPlayer
。 -
Get it from a Service Locator. So far, we're assuming the global class is some regular concrete class like
Game
. Another option is to define a class whose sole reason for being is to give global access to objects. This common pattern is called a Service Locator and gets its own chapter. -
通过服务定位器来访问。 到现在位置,我们假设全局类就是像
Game
那样的具体类。另外一个选择就是定义一个类专门用来给对象做全局访问。这个模式被称之为 服务定位器并有单独的章节。
The question remains, where should we use the real Singleton pattern? Honestly, I've never used the full Gang of Four implementation in a game. To ensure single instantiation, I usually simply use a static class. If that doesn't work, I'll use a static flag to check at runtime that only one instance of the class is constructed.
我们还有一个问题,我们什么情况下使用真正的单件呢?老实说,我重来没有在游戏中使用四人帮的全部实现。为了简单原则,我一般简单的使用一个静态类。如果这不能够满足,我将会使用一个静态的标志来在运行期检查只有一个类被创建了。
There are a couple of other chapters in this book that can also help here. The Subclass Sandbox pattern gives instances of a class access to some shared state without making it globally available. The Service Locator pattern does make an object globally available, but it gives you more flexibility with how that object is configured.
本书的一些其他章节也会有所帮助。 沙箱模式能够提供一些共享状态的访问指针而不必全局可见。本地模式是的一个对象全局可见,但是他给你这个物体更多弹性的配置。