解耦模式--服务定位器

理论要点

  • 什么是服务定位器模式:提供服务的全局接入点,而不必让用户和实现它的具体类耦合。通俗点讲就是服务类定义了一堆操作的抽象接口,具体的服务提供者实现这些接口。分离的定位器来管理服务类,外部就是通过这个定位器对象来间接获取服务

  • 要点
    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;
  }
};

嗯,服务定位器模式就先介绍到这了,和我们的单例模式很类似,核心就是多了个间接层:定位器!

猜你喜欢

转载自blog.csdn.net/u010223072/article/details/71213782