Provide a global point of access to a service without coupling users to the concrete class that implements it.
提供一个全局的访问服务的指针,并且使用者不会和具体实现类耦合
Some objects or systems in a game tend to get around, visiting almost every corner of the codebase. It's hard to find a part of the game that won't need a memory allocator, logging, or random numbers at some point. Systems like those can be thought of as services that need to be available to the entire game.
在游戏里的一些系统或者对象倾向于到处被使用,在代码库的每个角落都能看见。很难在游戏中找到某个部分, 不会使用内存分配器,记录日志,或者获取随机数。想这样的系统可以考虑作为一个服务,时能整个游戏都能够访问。
For our example, we'll consider audio. It doesn't have quite the reach of something lower-level like a memory allocator, but it still touches a bunch of game systems. A falling rock hits the ground with a crash (physics). A sniper NPC fires his rifle and a shot rings out (AI). The user selects a menu item with a beep of confirmation (user interface).
对我们而言,我举音频系统作为一个例子。它不会想内存分配器那么底层,但是任然要访问一批系统。掉落的石块碰到 地面上(物理系统)。一个狙击NPC开枪,发出短暂的枪声(AI系统)。用户选择一个菜单,伴随一个确认的音效(用户交互系统)。
Each of these places will need to be able to call into the audio system with something like one of these:
每个这样的地方都需要想这样调用音频系统:
// Use a static class?
AudioSystem::playSound(VERY_LOUD_BANG);
// Or maybe a singleton?
AudioSystem::instance()->playSound(VERY_LOUD_BANG);
Either gets us where we're trying to go, but we stumbled into some sticky
coupling along the way. Every place in the game calling into our audio system
directly references the concrete AudioSystem
class and the mechanism for
accessing it -- either as a static class or a singleton.
尽管我们到达了我们想要的目的,但是我们是在耦合的情况下一路走过来。在游戏每个调用音频系统的地方,都直接访问了
具体的AudioSystem
类和访问它的机制——要么作为一个静态类或者一个单件
These call sites, of course, have to be coupled to something in order to make a sound play, but letting them poke at the concrete audio implementation directly is like giving a hundred strangers directions to your house just so they can drop a letter on your doorstep. Not only is it a little bit too personal, it's a real pain when you move and you have to tell each person the new directions.
这些调用的地方,为了能够播放声音,都和某些东西耦合起来了,但是让它们能够直接操作具体的音频实现类就像为了 能够让信箱的信投递出去而让一百个陌生人能直接进入你家。不仅是这有点太私人了,而且当你搬家你必须高数每个 人你的新地址,这有点太痛苦了。
There's a better solution: a phone book. People that need to get in touch with us can look us up by name and get our current address. When we move, we tell the phone company. They update the book, and everyone gets the new address. In fact, we don't even need to give out our real address at all. We can list a P.O. box or some other "representation" of ourselves instead. By having callers go through the book to find us, we have a convenient single place where we control how we're found.
这里有个更好的解决办法:一个电话薄。每个想要联系你的人能够通过名字查看来得到新的地址。当我们搬家时,我们 能够告诉电话公司。它们更新电话薄,这样每个人都能得到新的地址了。世界上,我们甚至不必给出我们真正的地址。 我们能够列出一个P.O信箱,或者其他能够“代表”我们的东西。通过有一个扎到电话薄来联系我们的访问者,我们能够 方便的单独我们控制的能够查找的地方。
This is the Service Locator pattern in a nutshell -- it decouples code that needs a service from both who it is (the concrete implementation type) and where it is (how we get to the instance of it).
这就是服务定位器的简单介绍——它将一个服务的“是什么”和“在什么地方”与需要这个服务的代码解耦了。
A service class defines an abstract interface to a set of operations. A concrete service provider implements this interface. A separate service locator provides access to the service by finding an appropriate provider while hiding both the provider's concrete type and the process used to locate it.
一个服务类为一系列操作定义了一个抽象的接口。一个具体的服务提供者实现这个接口。一个单独的 服务定位器通过查找一个合适的提供者来提供这个服务的访问。它同时屏蔽了提供者的具体类型和定位这个服务的过程。
Anytime you make something accessible to every part of your program, you're asking for trouble. That's the main problem with the Singleton pattern, and this pattern is no different. My simplest advice for when to use a service locator is: sparingly.
每当你将东西变得全局都能访问的时候,你就是在自找麻烦。这就是单件模式主要的问题,但是这个模式不同。我对何时使用服务定 位器的简单建议就是:谨慎地使用。
Instead of using a global mechanism to give some code access to an object it needs, first consider passing the object to it instead. That's dead simple, and it makes the coupling completely obvious. That will cover most of your needs.
与提供一个全局机制来给需要使用的地方去访问一个对象不同,首先考虑将这个对象传递进去。这简单死了,而且明显将代码 耦合起来了。这件满足你绝大部分需求。
But... there are some times when manually passing around an object is gratuitous or actively makes code harder to read. Some systems, like logging or memory management, shouldn't be part of a module's public API. The parameters to your rendering code should have to do with rendering, not stuff like logging.
但是... 有时手动的将一个对象传来传去显得毫无理由也将代码变得难以阅读。游戏系统,比如日志系统和内存管理系统, 不应该是某个模块的公开API的一部分。你渲染代码的参数是和渲染相关的,而不是像日志那样的东西。
Likewise, other systems represent facilities that are fundamentally singular in nature. Your game probably only has one audio device or display system that it can talk to. It is an ambient property of the environment, so plumbing it through ten layers of methods just so one deeply nested call can get to it is adding needless complexity to your code.
同样的,也适用于一些单独的代表基础设施的系统。你的游戏很可能只有一个音频设备或者显示体统能够打交道。他是 一项环境属性,所以将它传递10层以便让一个底层的函数能够访问,为代码增加了毫无必要的复杂度。
In those kinds of cases, this pattern can help. As we'll see, it functions as a more flexible, more configurable cousin of the Singleton pattern. When used well, it can make your codebase more flexible with little runtime cost.
在这些情况下,这个模式能够起到作用。它用起来像一个更有弹性,更可配置的单间模式表亲。当我们良好地使用时, 它能让你的代码更有弹性,而且几乎没有运行损失。
Conversely, when used poorly, it carries with it all of the baggage of the Singleton pattern with worse runtime performance.
相反的,当我们使用不当时,它带来了所有单间模式的短板和糟糕的运行开销。
The core difficulty with a service locator is that it takes a dependency -- a bit of coupling between two pieces of code -- and defers wiring it up until runtime. This gives you flexibility, but the price you pay is that it's harder to understand what your dependencies are by reading the code.
服务定位器的关键困难在于,它要有所依赖——连接两份代码——并且在运行期才连接起来。这给你弹性,代价是阅读代码 时有点难以理解你依赖的是什么?
With a singleton or a static class, there's no chance for the instance we need to not be available. Calling code can take for granted that it's there. But since this pattern has to locate the service, we may need to handle cases where that fails. Fortunately, we'll cover a strategy later to address this and guarantee that we'll always get some service when you need it.
当使用单间或者一个静态类时,我们需要的实例没有机会变得不可用。调用代码就保证了它必须在哪里。但是,既然 这个模式定位服务,我们必须处理定位失败的情况。幸运的是,我们将讨论一个策略来 处理这个问题,并且保证我们始终在使用的时候得到某个服务。
Since the locator is globally accessible, any code in the game could be requesting a service and then poking at it. This means that the service must be able to work correctly in any circumstance. For example, a class that expects to be used only during the simulation portion of the game loop and not during rendering may not work as a service -- it wouldn't be able to ensure that it's being used at the right time. So, if a class expects to be used only in a certain context, it's safest to avoid exposing it to the entire world with this pattern.
既然定位器是全局可见的,游戏中的任何代码都有可能请求一个服务然后操作它。这意味着这个服务在任何情况下都 必须正确工作。举个例子,一个类只应当在游戏循环的仿真部分使用,而不是在渲染期间。就不能当做服务——它不能 保证它在正确的时机被使用。因此,如果一个类希望只在摸个特性的上下文种被使用,避免用这种模式将它暴露全局 是最安全的。
Getting back to our audio system problem, let's address it by exposing the system to the rest of the codebase through a service locator.
回到我们的音频系统问题,让我们通过服务定位器来讲他暴露给其他部分的代码。
We'll start off with the audio API. This is the interface that our service will be exposing:
我们从音频API开始。这就是我们服务将要暴露的接口:
class Audio
{
public:
virtual ~Audio() {}
virtual void playSound(int soundID) = 0;
virtual void stopSound(int soundID) = 0;
virtual void stopAllSounds() = 0;
};
A real audio engine would be much more complex than this, of course, but this shows the basic idea. What's important is that it's an abstract interface class with no implementation bound to it.
一个真正的音频引擎将比这个复杂的多,当然,这份代码表达了基本的原理。重要的一点就是它使一个虚 接口类,没有实现和它绑定。
By itself, our audio interface isn't very useful. We need a concrete implementation. This book isn't about how to write audio code for a game console, so you'll have to imagine there's some actual code in the bodies of these functions, but you get the idea:
仅是自己,我们的音频接口没有什么用处。我们需要一份具体的实现。本书不讨论怎样为一个游戏写音频代码,所以你 只能想象这些函数体中有一些真正的代码,不过你了解了原理。
class ConsoleAudio : public Audio
{
public:
virtual void playSound(int soundID)
{
// Play sound using console audio api...
}
virtual void stopSound(int soundID)
{
// Stop sound using console audio api...
}
virtual void stopAllSounds()
{
// Stop all sounds using console audio api...
}
};
Now we have an interface and an implementation. The remaining piece is the service locator -- the class that ties the two together.
现在我们有了一个接口和一份实现。剩下的部分就是服务定位器了——这个类将两者绑在一起。
The implementation here is about the simplest kind of service locator you can define:
下面的实现是你能够定义的最简单的服务定位器:
class Locator
{
public:
static Audio* getAudio() { return service_; }
static void provide(Audio* service)
{
service_ = service;
}
private:
static Audio* service_;
};
The technique this uses is called dependency injection, an awkward bit of jargon for a very simple idea. Say you have one class that depends on another. In our case, our
Locator
class needs an instance of theAudio
service. Normally, the locator would be responsible for constructing that instance itself. Dependency injection instead says that outside code is responsible for injecting that dependency into the object that needs it.
这里使用的技术叫做依赖注入,~~~TO-DO~~~。假设你有一个类,依赖另外一个。在我们的例子中,我们 的
Locator
类需要Audio
服务的一个实例。通常,这个定位器应该负责为自己构建这个实例。依赖注入却说外部 代码应该负责为这个对象注入它所需要的这个依赖。
The static getAudio()
function does the locating. We can call it from
anywhere in the codebase, and it will give us back an instance of our Audio
service to use:
简单函数getAudio()
做定位。我们能再代码的任何地方调用它,它能返回一个Audio
实例来供我们使用。
Audio *audio = Locator::getAudio();
audio->playSound(VERY_LOUD_BANG);
The way it "locates" is very simple -- it relies on some outside code to register a service provider before anything tries to use the service. When the game is starting up, it calls some code like this:
它“定位”的方法十分简单——它依赖一些外围代码在任何使用这个服务之前,注册一个服务提供者。当游戏启动之时, 它调用类似下面的代码:
ConsoleAudio *audio = new ConsoleAudio();
Locator::provide(audio);
The key part to notice here is that the code that calls playSound()
isn't aware of
the concrete ConsoleAudio
class; it only knows the abstract Audio
interface. Equally
important, not even the locator class is coupled to the concrete service
provider. The only place in code that knows about the actual concrete class is
the initialization code that provides the service.
这里关键需要注意的地方时调用playSound()
的代码对ConsoleAudio
具体实现毫不知情。同样重要的是,
甚至是定位器本身和具体服务提供者也没有联系。代码中唯一知道具体实现类的地方时,提供这个服务的初始化代码。
There's one more level of decoupling here: the Audio
interface isn't aware of
the fact that it's being accessed in most places through a service locator. As
far as it knows, it's just a regular abstract base class. This is useful because
it means we can apply this pattern to existing classes that weren't
necessarily designed around it. This is in contrast with Singleton, which affects the design of the "service"
class itself.
这里有还有更深一层的解耦——通过服务定位器Audio
接口在绝大数地方不知道自己正在被访问。一旦它知道了,
它就是一个普通的抽象基类了。这十分有用,因为它意味着我们可以将这个模式应用到一些已经纯在的但并
不是围绕这个来设计的类上。这和单件有个对比,后者影响了“服务”类本身的设计。
Our implementation so far is certainly simple, and it's pretty flexible too. But
it has one big shortcoming: if we try to use the service before a provider has been registered, it returns NULL
.
If the calling code doesn't check that, we're going to crash the game.
目前为止,我们的实现还很简单,不过也十分灵活。但是它有一个大的缺陷:如果我们尝试在一个服务提供者注册
之前使用它,它返回一个NULL
。如果我们的调用代码没有检查这一点,我们的游戏就会崩溃。
I sometimes hear this called "temporal coupling" -- two separate pieces of code that must be called in the right order for the program to work correctly. All stateful software has some degree of this, but as with other kinds of coupling, reducing temporal coupling makes the codebase easier to manage.
我有时听说这叫“时序耦合”——两份代码必须按正确的顺序调用来保证程序正确工作。每个状态软件都有不同程度的这个 问题,但是和其他耦合比较起来,消除时序耦合使得代码易于管理。
Fortunately, there's another design pattern called "Null Object" that we can use
to address this. The basic idea is that in places where we would return NULL
when we fail to find or create an object, we instead return a special object
that implements the same interface as the desired object. Its implementation
basically does nothing, but it allows code that receives the object to safely
continue on as if it had received a "real" one.
庆幸的是,这里有一个称之为“NULL Object”的模式来解决这个问题。基本的思想是在我们查找或者创建失败, 返回“NULL”的地方,我们返回一个实现同样接口的特殊对象作为替代。它地实现就是什么也不做,但是它能让 或者这个对象的代码正确的走下去,就好像它获得了一个“真正的”对象一样。
To use this, we'll define another "null" service provider: 为了使用它,我们定义另外一个“null”服务器。
class NullAudio: public Audio
{
public:
virtual void playSound(int soundID) { /* Do nothing. */ }
virtual void stopSound(int soundID) { /* Do nothing. */ }
virtual void stopAllSounds() { /* Do nothing. */ }
};
As you can see, it implements the service interface, but doesn't actually do anything. Now, we change our locator to this:
如你所见,它实现了服务结构,但是实际上什么也不做。现在我们来修改定位器:
class Locator
{
public:
static void initialize() { service_ = &nullService_; }
static Audio& getAudio() { return *service_; }
static void provide(Audio* service)
{
if (service == NULL)
{
// Revert to null service.
service_ = &nullService_;
}
else
{
service_ = service;
}
}
private:
static Audio* service_;
static NullAudio nullService_;
};
You may notice we're returning the service by reference instead of by pointer now. Since references in C++ are (in theory!) never
NULL
, returning a reference is a hint to users of the code that they can expect to always get a valid object back.
你可能注意到现在我们返回一个引用而不是一个指针。因为在C++中(理论上)一个引用永远不可能为
NULL
, 返回一个引用可以提示使用者它可以期望任何时候都返回一个有效的对象。
The other thing to notice is that we're checking for
NULL
in theprovide()
function instead of checking for the accessor. That requires us to callinitialize()
early on to make sure that the locator initially correctly defaults to the null provider. In return, it moves the branch out ofgetAudio()
, which will save us a couple of cycles every time the service is accessed.
另外需要注意的地方时,我们在
provide()
函数中检查是否为NULL
不不是在访问者中检查。这 要求我们尽早的调用initialize()
函数来保证定位器正确的初始化,默认指向空服务器。作为回报,它将 这个分支从getAudio()
中移开,为我们每次访问服务器节省了几次CPU循环周期。
Calling code will never know that a "real" service wasn't found, nor does it
have to worry about handling NULL
. It's guaranteed to always get back a valid
object.
调用代码永远也不会知道一个“真”的服务器没有找到,它也不必担心处理NULL
。它保证始终返回一个有效的对象。
This is also useful for intentionally failing to find services. If we want to disable a system temporarily, we now have an easy way to do so: simply don't register a provider for the service, and the locator will default to a null provider.
这也在有意的查找服务失败时有用。如果我们想要暂时的禁用一个系统,我们能够轻易的做到——简单的不为 这个服务注册服务器,然后定位器将默认返回一个空服务器。
Turning off audio is handy during development. It frees up some memory and CPU cycles. More importantly, when you break into a debugger just as a loud sound starts playing, it saves you from having your eardrums shredded. There's nothing like twenty milliseconds of a scream sound effect looping at full volume to get your blood flowing in the morning.
在开发过程中关闭音频是很便利的,它节约了一些内存和CPU周期。更重要的时,但你断进调试器的时候, 他会播发一个巨大的声音,它能防止你的耳膜破裂。再也没有什么在早晨能比20毫秒的一个满音量的音效尖叫让你的血液涌动了。
Now that our system is pretty robust, let's discuss another refinement this pattern lets us do -- decorated services. I'll explain with an example.
现在我们的系统十分强健,让我们讨论另外一项这个模式的优雅之处——装修服务器。我将举个例子做说明。
During development, a little logging when interesting events occur can help you figure out what's going on under the hood of your game engine. If you're working on AI, you'd like to know when an entity changes AI states. If you're the sound programmer, you may want a record of every sound as it plays so you can check that they trigger in the right order.
在开发中,一小段感兴趣的事件日志能够让你估摸出在游戏引擎外表之下发生了什么。如果你在开发AI系统, 你很想要知道一个单位的AI状态什么时候发生了变化。如果你是音频程序要,你可能想要知道每次声音播放的 记录,以便你能够检测触发器都在正确的位置上。
The typical solution is to litter the code with calls to some log()
function. Unfortunately, that replaces one problem with another -- now we have
too much logging. The AI coder doesn't care when sounds are playing,
and the sound person doesn't care about AI state transitions, but now they both
have to wade through each other's messages.
典型的解决方法是调用一些log()
函数。不幸的时,它用另一个问题替代了一个问题——现在我们有太多日志了。
AI程序员不关心声音什么时候播放,声音程序要不想知道AI状态的切换,但是现在他们都必须过滤各自的日志信息。
Ideally, we would be able to selectively enable logging for just the stuff we care about, and in the final game build, there'd be no logging at all. If the different systems we want to conditionally log are exposed as services, then we can solve this using the Decorator pattern. Let's define another audio service provider implementation like this:
理想状态下,我们能够为要关心的时间选择日志开启,并在游戏最后构建时,将没有任何日志。如果不同的系统的 条件日志作为服务器暴露出去,现在我们可以使用装饰器 模式解决这个问题。让我们像这样 定义另外一个音频实现:
class LoggedAudio : public Audio
{
public:
LoggedAudio(Audio &wrapped)
: wrapped_(wrapped)
{}
virtual void playSound(int soundID)
{
log("play sound");
wrapped_.playSound(soundID);
}
virtual void stopSound(int soundID)
{
log("stop sound");
wrapped_.stopSound(soundID);
}
virtual void stopAllSounds()
{
log("stop all sounds");
wrapped_.stopAllSounds();
}
private:
void log(const char* message)
{
// Code to log message...
}
Audio &wrapped_;
};
As you can see, it wraps another audio provider and exposes the same interface. It forwards the actual audio behavior to the inner provider, but it also logs each sound call. If a programmer wants to enable audio logging, they call this:
如你所见,它包装了另外一个音频器并暴露了同样的接口。它将实际的音频操作转发给内嵌的服务器,但是它同时 记录了每次音频调用。如果一个程序要需要开启音频日志,他这样调用代码:
void enableAudioLogging()
{
// Decorate the existing service.
Audio *service = new LoggedAudio(Locator::getAudio());
// Swap it in.
Locator::provide(service);
}
Now, any calls to the audio service will be logged before continuing as before. And, of course, this plays nicely with our null service, so you can both disable audio and yet still log the sounds that it would play if sound were enabled.
现在,任何音频服务的调用在之前运行之前会被记录。同时,当然,这和我们的空服务器合作良好,所以你可以 即关闭音频又仍然开启声音日志,如果声音开启,它将会播放声音。
We've covered a typical implementation, but there are a couple of ways that it can vary based on differing answers to a few core questions:
我们讨论了一个典型的实现,对一些核心问题,不同的方式会有不同的答案。
-
Outside code registers it:
-
在外部代码注册:
This is the mechanism our sample code uses to locate the service, and it's the most common design I see in games:
这是我们简答的代码用来定位服务器的机制,同时这也是我在游戏中最常见的设计。
-
It's fast and simple. The
getAudio()
function simply returns a pointer. It will often get inlined by the compiler, so we get a nice abstraction layer at almost no performance cost. -
它简单快捷。
getAudio()
函数简单的返回一个指正,它通常被编译器内联,所以我们 得到了一个良好的抽象层最好没有性能损失。 -
We control how the provider is constructed. Consider a service for accessing the game's controllers. We have two concrete providers: one for regular games and one for playing online. The online provider passes controller input over the network so that, to the rest of the game, remote players appear to be using local controllers.
-
*我们能共控制服务器如何构建。*考虑一个服务访问游戏的控制者。我们有两个具体的服务器:一个 是通常游戏,一个是在线游戏。在线服务器将控制者操作传递到网络上,以便,对其他部分,远程玩家 就像使用本地控制器一样。
To make this work, the online concrete provider needs to know the IP address of the other remote player. If the locator itself was constructing the object, how would it know what to pass in? The
Locator
class doesn't know anything about online at all, much less some other user's IP address.为了达到这点,在线服务器实现需要知道IP其他远程玩家的地址。如果定位器构建这个对象,它如何知道 什么需要传递进去呢?
Locator
这个类对在线一无所知,更何况其他用户的IP地址了。Externally registered providers dodge the problem. Instead of the locator constructing the class, the game's networking code instantiates the online-specific service provider, passing in the IP address it needs. Then it gives that to the locator, who knows only about the service's abstract interface.
外部注册服务器避开了这个问题。与其在定位器初始化这个类,游戏的网络代码初始化在线服务器, 将它需要的IP地址传递进去。然后将它转给定位器,而定位器只知道这个服务的抽象接口。
-
We can change the service while the game is running. We may not use this in the final game, but it's a neat trick during development. While testing, we can swap out, for example, the audio service with the null service we talked about earlier to temporarily disable sound while the game is still running.
-
*我们可以在游戏运行额时候更换服务器。*我们可能在最后游戏中不利用这一点,但是在开发中这是一个 很贴心的技巧。当测试时,我们可以切换。举个例子,我们之前讨论的代空服务的音频服务器可以在游戏 仍在运行的时间暂时禁止音频。
-
The locator depends on outside code. This is the downside. Any code accessing the service presumes that some code somewhere has already registered it. If that initialization doesn't happen, we'll either crash or have a service mysteriously not working.
-
*定位器依赖外部代码。*这是短板。访问服务的任何代码都假设其他代码已经注册过这个服务了。如果 没有发生初始化,我么要么崩溃,要么服务神秘地无法工作。
-
-
Bind to it at compile time:
-
在编译器绑定:
The idea here is that the "location" process actually occurs at compile time using preprocessor macros. Like so:
这里的想法是“定位”这个工作实际上发生在编译器,使用条件编译。想这样:
class Locator { public: static Audio& getAudio() { return service_; } private: #if DEBUG static DebugAudio service_; #else static ReleaseAudio service_; #endif };
Locating the service like this implies a few things:
像这样定位服务器指明了几点:
-
It's fast. Since all of the real work is done at compile time, there's nothing left to do at runtime. The compiler will likely inline the
getAudio()
call, giving us a solution that's as fast as we could hope for. -
*它十分快速。*既然所有的实际工作都发生在编译期,在运行期就没什么事情了。编译器很可能 内联
getAudio()
调用,这是我们能够到达最快。 -
You can guarantee the service is available. Since the locator owns the service now and selects it at compile time, we can be assured that if the game compiles, we won't have to worry about the service being unavailable.
-
*你能保证服务可用。*既然定位器现在拥有服务器并在编译器选择它,我们能保证如果游戏编译,我们 不比担心服务不可用。
-
You can't change the service easily. This is the major downside. Since the binding happens at build time, anytime you want to change the service, you've got to recompile and restart the game.
-
*你不能方便的更改服务器。*这是主要的缺点。应为绑定发生在构建期,任何你想要变动服务器,你必须 重新编译再重启游戏。
-
-
Configure it at runtime:
-
在运行期配置:
Over in the khaki-clad land of enterprise business software, if you say "service locator", this is what they'll have in mind. When the service is requested, the locator does some magic at runtime to hunt down the actual implementation requested.
在企业级商业软件中,如果你说“服务定位器”,这是它们需要要知道的。当服务被请求时,定位器做一些 在运行时魔法操作来定位时间被请求的实现。
Reflection is a capability of some programming languages to interact with the type system at runtime. For example, we could find a class with a given name, find its constructor, and then invoke it to create an instance.
反射是一些语言在运行期能和类型系统交互的能力。比如,我们能通过给定的名查找一个类,找到它的 构造器,然后调用构造器来创建一个实例。
Dynamically typed languages like Lisp, Smalltalk, and Python get this by their very nature, but newer static languages like C# and Java also support it.
动态类型语言,比如Lisp,Smalltalk,和Python能够十分自然的处理这点,但是新的静态类型语言比如C# 和Java也支持它。
Typically, this means loading a configuration file that identifies the provider and then using reflection to instantiate that class at runtime. This does a few things for us:
通常来说,这表示加载一份配置文件来决定定位器,然后使用反射来在运行期初始化这个类。这为我们 做了一些事情。
-
We can swap out the service without recompiling. This is a little more flexible than a compile-time-bound service, but not quite as flexible as a registered one where you can actually change the service while the game is running.
-
*我们不需重编译就能切换服务器。*这要比编译期绑定更具有弹性,但是比不上一个注册的服务器。它实际上 能在游戏运行的时候更换服务器。
-
Non-programmers can change the service. This is nice for when the designers want to be able to turn certain game features on and off but aren't comfortable mucking through source code. (Or, more likely, the coders aren't comfortable with them mucking through it.)
-
非程序员能够更换服务器。 这在设计人员想要开关游戏的某项特性,但是不能够安然地摆弄代码 时十分有用。(后者,更可能是,程序员对他们摆弄代码感到不安)
-
The same codebase can support multiple configurations simultaneously. Since the location process has been moved out of the codebase entirely, we can use the same code to support multiple service configurations simultaneously.
-
*一份代码库能够同时支持多份配置。*因为定位过程完全移除代码库,我们能够使用同样的代码同时支持 多个服务配置文件。
This is one of the reasons this model is appealing over in enterprise web-land: you can deploy a single app that works on different server setups just by changing some configs. Historically, this was less useful in games since console hardware is pretty well-standardized, but as more games target a heaping hodgepodge of mobile devices, this is becoming more relevant.
这也是这个模式在企业级web开发中应用的原因:你能够发布单个app就能在不同耳朵服务器上工作, 只需要修改几个配置。历史上,这在游戏中没有什么用处,因为游戏终端硬件都是十分标准化的,但是 随着更多游戏开始瞄向杂乱的移动设备,这变得越来越有意义。
-
It's complex. Unlike the previous solutions, this one is pretty heavyweight. You have to create some configuration system, possibly write code to load and parse a file, and generally do some stuff to locate the service. Time spent writing this code is time not spent on other game features.
-
这比较复杂。不像前几个解决方案,这十分重量级。你必须创建摸个配置系统,很可能写代码 去加载解析文件,并通常做某些操作定位服务器。化在写这写代码上的时间就不能用来写别的 游戏特性了。
-
Locating the service takes time. And now the smiles really turn to frowns. Going with runtime configuration means you're burning some CPU cycles locating the service. Caching can minimize this, but that still implies that the first time you use the service, the game's got to go off and spend some time hunting it down. Game developers hate burning CPU cycles on something that doesn't improve the player's game experience.
-
定位服务需要时间。 现在,是到真正皱眉了。使用运行期配置意味着你在定位服务时燃烧CPU周期。 缓存能减缓这点,但是仍然影响你第一次使用这个服务的时候,游戏需要挂起花费时间来处理它。游戏 程序要痛恨浪费CPU周期在不能提高游戏体验的事情上。
-
-
Let the user handle it:
-
让使用者处理:
The simplest solution is to pass the buck. If the locator can't find the service, it just returns
NULL
. This implies:简单的方法就是转移责任。如果定位器找不到服务器,它就返回
NULL
。这表示:-
It lets users determine how to handle failure. Some users may consider failing to find a service is a critical error that should halt the game. Others may be able to safely ignore it and continue. If the locator can't define a blanket policy that's correct for all cases, then passing the failure down the line lets each call site decide for itself what the right response is.
-
它让使用者决定如何处理失败。 有些使用者可能认为查找服务失败是一个严重错误,需要终止游戏。 其他或许认为能安全地忽略它并继续执行。如果定位器不能定义一个全面的策略,对每种情况都正确, 那么将失败传递给调用者,来决定正确的相应。
-
Users of the service must handle the failure. Of course, the corollary to this is that each call site must check for failure to find the service. If almost all of them handle failure the same way, that's a lot duplicate code spread throughout the codebase. If just one of the potentially hundreds of places that use the service fails to make that check, our game is going to crash.
-
服务使用者必须处理失败。当然,必然的结果就是每处调用点必须检测查找服务失败。如果几乎每处处理 失败方式都一样,这就是重复代码传播在代码库中。如果几百处潜在的地方又一次没有做错误检测,我们游戏 就可能会崩溃。
-
-
Halt the game:
-
终止游戏:
I said that we can't prove that the service will always be available at compile-time, but that doesn't mean we can't declare that availability is part of the runtime contract of the locator. The simplest way to do this is with an assertion:
我说过,我们不能证明服务器在编译器不能始终有效,但这并不意味这我能不能声明可用性是定位器的 责任。最简单的方法是写一个断言:
class Locator { public: static Audio& getAudio() { Audio* service = NULL; // Code here to locate service... assert(service != NULL); return *service; } };
If the service isn't located, the game stops before any subsequent code tries to use it. The
assert()
call there doesn't solve the problem of failing to locate the service, but it does make it clear whose problem it is. By asserting here, we say, "Failing to locate a service is a bug in the locator."如果服务没有找到,游戏在任何后续代码使用之前停止。
assert()
调用并没有解决查找服务失败的问题,但是 它明确了这是谁的问题。通过断言,我们说,“定位服务失败是定位器的一个bug”。The Singleton chapter explains the
assert()
function if you've never seen it before.如果你之前没有看见过
assert()
这个函数 单件模式 这章它的解释So what does this do for us?
所以对我们来说该怎么做呢?
-
Users don't need to handle a missing service. Since a single service may be used in hundreds of places, this can be a significant code saving. By declaring it the locator's job to always provide a service, we spare the users of the service from having to pick up that slack.
-
*使用者不需要处理一个遗失的服务。*应为一个服务可能用到上百处,这能节省很多代码。通过 声明总是提供一个服务是定位器的工作。我们让服务使用者清闲下来。
-
The game is going to halt if the service can't be found. On the off chance that a service really can't be found, the game is going to halt. This is good in that it forces us to address the bug that's preventing the service from being located (likely some initialization code isn't being called when it should), but it's a real drag for everyone else who's blocked until it's fixed. With a large dev team, you can incur some painful programmer downtime when something like this breaks.
-
如果服务没有找到,游戏将会中断。 在极少的情况下,如果服务真的早不到,游戏就会关闭。它强制我们 去寻找那些阻止服务被发现的Buf来说是很好的(比如一些初始化代码没有被正确调用),但是这对那些堵在 修复的那些人确实是个泥潭。如果有一个大的开发组,当这些东西发生时,你可以增加一些苦逼程序员的停工时间。
-
-
Return a null service: **返回一个空服务:
We showed this refinement in our sample implementation. Using this means: 我们在我们的简单实现中展示了这种优雅的实现。这种方案意味着:
-
Users don't need to handle a missing service. Just like the previous option, we ensure that a valid service object will always be returned, simplifying code that uses the service.
-
*使用者不需要处理丢失的服务。*就先之前的一个选项一样,我们确保始终返回一个有效的服务,简化 了使用者的代码。
-
The game will continue if the service isn't available. This is both a boon and a curse. It's helpful in that it lets us keep running the game even when a service isn't there. This can be really helpful on a large team when a feature we're working on may be dependent on some other system that isn't in place yet.
-
*当服务不可用时,游戏还能继续。*这是一把双刃剑。这是我们的游戏在没有服务的时候也能运行。这对一个 大团队,当我们依赖的一个特性还没有被其他人开发出来是特别有用。
The downside is that it may be harder to debug an unintentionally missing service. Say the game uses a service to access some data and then make a decision based on it. If we've failed to register the real service and that code gets a null service instead, the game may not behave how we want. It will take some work to trace that issue back to the fact that a service wasn't there when we thought it would be.
这个副作用就是,在非特意的丢失服务时难以跟踪。假设游戏使用一个服务来访问某些数据然后根据 这些数据做一些决定。如果我们没有注册真正的服务,让代码得到了一个空服务,游戏就不会像预计那样运作。 找到真相需要花费一些时间,原来是服务没有想我们想的那样可用。
>We can alleviate this by having the null service print some debug output whenever it's used.我们可以让空服务在任何使用的时候打印debug日子来缓解这点。
-
Among these options, the one I see used most frequently is simply asserting that the service will be found. By the time a game gets out the door, it's been very heavily tested, and it will likely be run on a reliable piece of hardware. The chances of a service failing to be found by then are pretty slim.
在这些选项中,我见到使用最多的就是断言服务能够找到。当游戏发布的时候,游戏被仔细测试过了,并且会在一个可靠 的设备上运行。服务没有找到的机会十分渺小。
On a larger team, I encourage you to throw a null service in. It doesn't take much effort to implement, and can spare you from some downtime during development when a service isn't available. It also gives you an easy way to turn off a service if it's buggy or is just distracting you from what you're working on.
在大点的团队中,我推荐你使用一个空服务。它不需要花费什么功夫就能实现,而且可以让你在其他服务不可用时的停工 中解脱出来。它也给你提供了便利的方式来关闭服务,如果这个服务有bug或者就是打扰了你的工作。
Up to this point, we've assumed that the locator will provide access to the service to anyone who wants it. While this is the typical way the pattern is used, another option is to limit access to a single class and its descendants, like so:
到目前为止,我们假设定位器为每个使用它的代码提供访问。即使这是这个模式典型的使用方式,另外一个选项是 限制它的访问到单个类和他的依赖类中,比如:
class Base
{
// Code to locate service and set service_...
protected:
// Derived classes can use service
static Audio& getAudio() { return *service_; }
private:
static Audio* service_;
};
With this, access to the service is restricted to classes that inherit Base
.
There are advantages either way:
通过这点,访问服务被定向到继承了Base
的类中。这不管怎么说都有两点好处:
-
If access is global:
-
如果是全局访问:
-
It encourages the entire codebase to all use the same service. Most services are intended to be singular. By allowing the entire codebase to have access to the same service, we can avoid random places in code instantiating their own providers because they can't get to the "real" one.
-
*它鼓励了整个代码库使用同一个服务。*大部分服务都应该是单独的。允许整个代码库访问同一个服务,我们 能避免代码中随机的初始化它们各自的提供者,应为他们不能得到一个”真正“的服务。
-
We lose control over where and when the service is used. This is the obvious cost of making something global -- anything can get to it. The Singleton chapter has a full cast of characters for the horror show that global scope can spawn.
-
*我们对何时何地使用完全失去了控制。*这是将东西变为全局的代价——任何人都能访问。 单件这章将花费一整章来讨论全局 作用域带来的可怕后果。
-
-
If access is restricted to a class:
-
如果访问定向到类中:
-
We control coupling. This is the main advantage. By limiting a service to a branch of the inheritance tree, we can make sure systems that should be decoupled stay decoupled.
-
*我们控制耦合。*这是主要的优势。通过限制服务到一个继承数上,我们能确保系统改解耦的地方解耦了。
-
It can lead to duplicate effort. The potential downside is that if a couple of unrelated classes do need access to the service, they'll each need to have their own reference to it. Whatever process is used to locate or register the service will have to be duplicated between those classes.
-
它可能导致重复的工作。潜在的缺点是,如果有去多不相干的类确实需要访问服务,它们需要有各自的 引用。那些定位和注册服务的工作在这些类中都要重复的处理。
(The other option is to change the class hierarchy around to give those classes a common base class, but that's probably more trouble than it's worth.) (其他的选项就是修改类的继承,给这些类一个公共的基类,但是这会导致更多的问题。)
-
My general guideline is that if the service is restricted to a single domain in the game, then limit its scope to a class. For example, a service for getting access to the network can probably be limited to online classes. Services that get used more widely like logging should be global.
我通常的准则就是,如果服务被定向到一个单独的域中,就限制到它的作用域到类中。比如,获取网络访问的服务就 很可能限制在联网的类中。更广泛使用的服务,比如日志服务应该是全局的。
-
The Service Locator pattern is a sibling to Singleton in many ways, so it's worth looking at both to see which is most appropriate for your needs.
-
服务定位器模式在很多方面和单件是表亲,所以值得观察两者来决定谁更贴合你的需求。
-
The Unity framework uses this pattern in concert with the Component pattern in its
GetComponent()
method. -
Unity框架把这个模式和 组件 模式结合起来,使用在
GetComponent()
方法中。 -
Microsoft's XNA framework for game development has this pattern built into its core
Game
class. Each instance has aGameServices
object that can be used to register and locate services of any type. -
Microsoft的 XNA游戏开发框架将这个模式内嵌到它的核心
Game
类中。每个实例有一个GameServices
对象,能够用来注册和定位任何类型的服务。