为某服务提供一个全局访问入口来避免使用者与该服务具体实现类之间产生耦合。

(摘自《游戏编程模式》)

对于一些在游戏系统各处都可能调用的系统、对象或函数,如果(利用静态类或单例模式)直接调用也会造成一定的耦合——当我们对静态类或单例模式的调用方式进行修改时,我们就需要找到各个调用的代码块并进行修改,而使用服务定位器模式可以解决这个问题。

你可以把服务定位当成一个单例模式的集合类,在服务定位器类中,可以汇集大量的静态实例。当我们需要某个实例是,通过静态变量即可获取并调用。

示例代码

简单的定位器类

class Audio
{
    public:
        virtual ~Audio(){}
        virtual void playSound(int soundID)=0;
        virtual void stopSound(int soundID)=0;
        virtual void stopAllSounds()=0;
}

//简单的定位器
class Locator
{
    public:
        static Audio* getAudio(){return service_;}
        
        static void provide(Audio* service)
        {
            service_=service;
        }
        
        private:
            static Audio* service_;
}

因此,我们可以通过Locator类来调用Audio实例。但要注意的是,若要正常调用,需要在游戏运行前注册一个服务提供器(调用provide注册)。但是,简单的代码总会有一些缺陷。例如在上面的代码中,当我们从未注册服务提供器时,getAudio()会返回nullptr。

空对象模式

每一次在调用服务定位器时要对返回值进行nullptr的检查很麻烦(毕竟每一行调用都有个 if 就很不优雅),空对象模式可以解决这个问题。“空对象”不是真正的空对象,而是作为一个对象,它的函数不会做任何操作。下面是示例代码。

class Audio
{
    public:
        virtual ~Audio(){}
        virtual void playSound(int soundID)=0;
        virtual void stopSound(int soundID)=0;
        virtual void stopAllSounds()=0;
}

//'空对象'
class NullAudio : Audio
{
    public:
        virtual ~Audio(){}
        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(sevice==nullptr)
                service=&nullService_;
            service_=service;
        }
        
        private:
            static Audio* service_;
            static NullAudio nullService_;
}

在游戏运行前,需要先执行Initialize即可。即使我们未注册服务提供器,也不会返回一个nullptr。

服务定位器的日志装饰器

我们对服务定位器应该有了更新的认识。实质上,服务定位器就是一个可以配置的单例模式。我们可以通过注册不同版本德服务提供器,来进行自定义的开发。例如,在游戏引擎中,有时候我们需要打开日志,有时候需要关闭日志来忽略一些问题,如何实现呢?我们继续优化服务定位器。示例代码如下所示:

class Audio
{
    public:
        virtual ~Audio(){}
        virtual void playSound(int soundID)=0;
        virtual void stopSound(int soundID)=0;
        virtual void stopAllSounds()=0;
}

//'空对象'
class NullAudio : Audio
{
    public:
        virtual ~Audio(){}
        virtual void playSound(int soundID){}
        virtual void stopSound(int soundID){}
        virtual void stopAllSounds(){}        
}

//带日志的音频模块
class LoggedAudio : public Audio
{
    public:
        LoggedAudio(Audio& audio) : wrapped(audio){}
        
        virtual void playSound(int soundID)
        {
            log("playSound : {soundID}");
            wrapped->playSound();
        }
        
        virtual void stopSound(int soundID)
        {
            log("stopSound : {soundID}");
            wrapped->stopSound();
        }
        
        virtual void stopAllSounds()
        {
            log("stopAllSounds : {soundID}");
            wrapped->stopAllSounds();
        }

    private:
        Audio* wrapped;
}


class Locator
{
    public:
        static void Initialize(){service_=&nullService_;} 
        static Audio* getAudio(){return service_;}
        
        static void provide(Audio* service)
        {
            if(sevice==nullptr)
                service=&nullService_;
            service_=service;
        }
        
        private:
            static Audio* service_;
            static NullAudio nullService_;
}

只要将LoggedAudio类型的实例注册至服务定位器,即可开启带日志的音频系统。