zoukankan      html  css  js  c++  java
  • 解耦模式--服务定位器

    理论要点

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

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

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

  • 相关阅读:
    MySQL之Web乱码问题
    MySQL之表操作
    Python学习笔记调式之抛出异常
    Python学习笔记调试之取得反向跟踪的字符串
    MySQL之库操作
    C#基础 冒泡排序
    C#基础 数组、二维数组
    C#基础 类及常用函数【string 、Math 、DiteTime 、TimeSpan】
    C#基础 异常语句 、跳转语句、while循环、穷举法、迭代法
    C#基础 循环语句【for】
  • 原文地址:https://www.cnblogs.com/cxx-blogs/p/9159255.html
Copyright © 2011-2022 走看看