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.
尽管一再告诫,在GoF的单件模式描述中,它通常缺点大于优点。 他们一再强调这个模式应当谨慎的使用,但是当应用在游戏行业时,这个强调通常被忽略了。
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.
当游戏还是车库里的几个家伙写的时候,推动硬件要比象牙塔软件工程准则更为重要。老式C语言和汇编程序员使用全局和静态代码而没有任何问题,并开发出优秀的游戏。随着游戏变得更大更复杂,架构和可维护性开始变为短板。我们挣扎着开发游戏不是因为硬件限制,而是因为开发效率限制。
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()
这个调用。为了搞清楚其中发生了什么,我们需要查看整个代码库来看是谁访问了全局状态。在你不得不凌晨3点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.
这个问题有几个拓展的答案(本书的绝大部分从某些方面来说就是这个),但是它们不明显或者简单能够得到。 与此同时,我们需要发布我们的游戏。单件模式就像一帖万能药。它在一本关于面向对象设计模式书中,所以它肯定是 架构合理的,对吧?并且它能像之前我们开发了多年那样去设计软件。
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.
在GoF的单件模式中那个“并”这个词有点奇怪。这个模式解决的是一个问题还是两个问题?如果我们只用 其中的一个问题怎么办?确保一个实例是很有用的,但是谁说我们想要任何人都能操作它?就好比, 全局访问是很方便,但是允许有多个实例却是很常见的。
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.
这两个问题的后者,便利的访问,是我们使用单件模式的主要原因。比如一个日志类。许多游戏中的模块都能够从日志模块记录诊断信息中获得好处。但是,将我们Log
类的实例传递给每个函数扰乱了函数签名,并与代码意图分散。
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
的东西也需要这个管理器,而你需要有多个BulletManager
实例呢?
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.
我们还有一个问题,我们应该在什么情况下使用真正的单件呢?老实说,我从来没有在一个游戏中使用GoF的每个模式。为了确保只实例化一次,我通常只是简单地使用一个静态类。如果那不起作用,我就会用一个静态的标识位在运行时检查是否只有一个实例被创建。
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.
本书的一些其他章节也会有所帮助。子类沙箱模式能够提供一些共享状态的访问指针而不必全局可见。服务定位器模式确实让一个对象全局可见,但是给了你更灵活的方法去配置。