理论要点
什么是服务定位器模式:提供服务的全局接入点,而不必让用户和实现它的具体类耦合。通俗点讲就是服务类定义了一堆操作的抽象接口,具体的服务提供者实现这些接口。分离的定位器来管理服务类,外部就是通过这个定位器对象来间接获取服务。
要点
1,一般通过使用单例或者静态类来实现服务定位模式,提供服务的全局接入点。和单例模式很像,只是多了一个间接获取服务对象的中间管理类。不让用户直接接触具体服务类。2,服务定位模式可以看做是更加灵活,更加可配置的单例模式。如果用得好,它能以很小的运行时开销,换取很大的灵活性。相反,如果用得不好,它会带来单例模式的所有缺点以及更多的运行时开销。
3,使用服务定位器的核心难点是它将依赖,也就是两块代码之间的一点耦合,推迟到运行时再连接。这有了更大的灵活度,但是代价是更难在阅读代码时理解其依赖的是什么。
使用场合
1,服务定位模式在很多方面是单例模式的亲兄弟,在应用前应该考虑看看哪个更适合你的需求。2,让大量内容在程序的各处都能被访问时,就是在制造混乱。对何时使用服务定位模式的最简单的建议就是:尽量少用。
3,与其使用全局机制让某些代码直接接触到它,不妨先考虑将对象传过来。因为这样可以明显地保持解耦,而且可以满足我们大部分的需求。当然,有时候不方便手动传入对象,也可以使用单例的方式。
4,Unity引擎在它的GetComponent()方法中使用了这个模式,协助组件模式的使用,方便随时获取到指定的组件。还有微软的XNA框架将这个模式内嵌到它的核心类Game中。每个实例有一个 GameServices 对象,能够用来注册和定位任何类型的服务。
代码分析
1,首先我们来看看这样一个游戏情节,还是以音频系统为例。我们游戏中很多系统都要访问它。 滚石撞击地面(物理)。 NPC狙击手开了一枪,射出子弹(AI)。 用户选择菜单项需要响一声确认(用户界面)。。。每处都需要像下面这样调用音频系统:
// 使用静态类?
AudioSystem::playSound(VERY_LOUD_BANG);
// 还是使用单例?
AudioSystem::instance()->playSound(VERY_LOUD_BANG);
尽管这样可以获得我们想要的结果,但是这里有些微妙的耦合。每个调用音频系统的游戏部分直接引用了具体的AudioSystem类。这里的音频系统我们可以看做是服务提供者,而游戏很多模块使用处则是服务使用者,想想其中的问题:首先明显的是隐私问题,服务提供者直接暴露给了很多使用者。然后是耦合问题,一旦服务提供者发生修改,那么所有的使用者都需要对应修改。
2,既然问题已经知道了,那么解决起来就很容易了,无非是想办法让服务提供者类和使用者解耦。那么只需要有个中间对象来连接它们两者就可以了,这个中间对象就是我们这节讲的服务定位器。
我们以音频服务为例,首先来看看这个服务提供者怎么实现,即我们的音频系统:
下面是服务要暴露的接口:
class Audio
{
public:
virtual ~Audio() {}
virtual void playSound(int soundID) = 0;
virtual void stopSound(int soundID) = 0;
virtual void stopAllSounds() = 0;
};
然后是服务具体提供者,继承实现这些接口:
class ConsoleAudio : public Audio
{
public:
virtual void playSound(int soundID)
{
// 使用主机音频API播放声音……
}
virtual void stopSound(int soundID)
{
// 使用主机音频API停止声音……
}
virtual void stopAllSounds()
{
// 使用主机音频API停止所有声音……
}
};
好,现在服务提供者已经实现了,接下来就是我们的服务定位器了。
class Locator
{
public:
static Audio* getAudio() { return service_; }
static void provide(Audio* service)
{
service_ = service;
}
private:
static Audio* service_;
};
服务对象由外部传入,使用关系就从这样了:服务类<—>定位器<—>使用者。
外部使用:
Audio *audio = Locator::getAudio();
audio->playSound(VERY_LOUD_BANG);
定位器初始化:
ConsoleAudio *audio = new ConsoleAudio();
Locator::provide(audio);
嗯,这个简单的服务定位器就已经粗略实现了。不过还有个明显的漏洞,看看如果定位器没有初始化我们就使用了它,那就直接崩溃了。
下面我们来做个安全操作,实现一个空服务:
class NullAudio: public Audio
{
public:
virtual void playSound(int soundID) { /* 什么也不做 */ }
virtual void stopSound(int soundID) { /* 什么也不做 */ }
virtual void stopAllSounds() { /* 什么也不做 */ }
};
现在,我们将服务定位器修改下:
class Locator
{
public:
static void initialize() { service_ = &nullService_; }
static Audio& getAudio() { return *service_; }
static void provide(Audio* service)
{
if (service == NULL)
{
// 退回空服务
service_ = &nullService_;
}
else
{
service_ = service;
}
}
private:
static Audio* service_;
static NullAudio nullService_;
};
调用代码永远不知道“真正的”服务没找到,也不必担心处理NULL。 这保证了它永远能获得有效的对象。
其实避免空对象还有一个思路,就是跳过运行时定位器初始化过程,让它在编译时就初始化,就像这样:
class Locator
{
public:
static Audio& getAudio() { return service_; }
private:
#if DEBUG
static DebugAudio service_;
#else
static ReleaseAudio service_;
#endif
};
它快速,也能保证服务一直可用,但是它没法改变服务了。一般这种方式我们使用的少,还是运行时初始化灵活性更大,至于避免空指针报错,上面说了创建一个什么也不做的空对象,然而这样对于查错不方便,有时我们可以直接使用断言,让游戏停止,暴露错误:
class Locator
{
public:
static Audio& getAudio()
{
Audio* service = NULL;
// Code here to locate service...
assert(service != NULL);
return *service;
}
};
嗯,服务定位器模式就先介绍到这了,和我们的单例模式很类似,核心就是多了个间接层:定位器!