Поиск службы (Service Locator)

Задача

Обеспечить глобальную точку доступа к службе без привязки пользователя к конкретному классу, который ее реализует.

Мотивация

Некоторым объектам или системам в игре может понадобиться посещать практически все уголки кодовой базы. Тяжело найти часть игры, которой не требуется выделять память, вести журнал, работать с файловой системой или генерировать случайные числа. О таких системах можно думать как о сервисах, которые должны быть доступны всей игре.

В нашем примере, мы будем работать с аудио. Это конечно не что-то настолько низкоуровневое как выделение памяти, но все равно затрагивает кучу игровых систем: Падающий камень ударяется об землю (физика). Снайпер NPC стреляет из винтовки (AI). Пользователь выбирает элемент меню с бипающим подтверждением (пользовательский интерфейс).

В каждом из этих мест нам нужно иметь возможность обращаться к аудио системе примерно таким образом:

// Используем статический класс?
AudioSystem::playSound(VERY_LOUD_BANG);

// Или может быть синглтон?
AudioSystem::instance()->playSound(VERY_LOUD_BANG);

Неважно каким путем мы пойдем, потому что в любом случае мы столкнемся с увеличением связности. Каждый участок кода в нашей игре может вызывать напрямую класс AudioSystem и для этого можно использовать механизм статического класса или синглтона (singleton)GoF.

Эти вызовы естественно должны быть к чему-то привязаны чтобы звук проигрывался, но позволять всем обращаться к вполне конкретной аудио реализации - это все равно что давать сотне незнакомцев указания как добраться до вашего дома чтобы они могли оставить письмо у вас на ступеньках. Это не просто слишком персонализированное обращение, это крайне неудачная идея если вам придется потом сообщать всем как добраться до нового места.

Есть решение получше: телефонная книга. Люди, которым нужно с нами связаться могут найти наше имя и получить наш адрес. Когда мы переезжаем, мы сообщаем об этом телефонной компании. Они обновляют книгу и все снова могут получить правильный адрес. Кроме того мы можем вообще не сообщать реальный адрес. Мы можем просто завести специальный почтовый ящик, который будет нас "представлять". Позволяя нашим респондентам находить нас по книге мы организуем единое место, управляющее тем как нас найти.

В этом и заключается суть шаблона Поиск службы: он отвязывает код, которому нужна служба от того чем она является (Тип конкретной реализации) и где находится (как получить экземпляр).

Шаблон

Класс служба определяет абстрактный интерфейс для набора операций. Конкретный поставщик службы (service provider) реализует этот интерфейс. Отдельный поиск службы (service locator) предоставляет доступ к службе и занимается поиском нужного поставщика и скрывает конкретный тип поставщика и процесс его поиска.

Когда использовать

Каждый раз, когда у вас появляется нечто, доступное для каждой части вашей программы - вы напрашиваетесь на проблемы. Это основная проблема шаблона синглтон (singleton)GoF и этот шаблон в этом плане ничем от него не отличается. Мой основной совет насчет того когда использовать шаблон: пореже.

Вместо того чтобы использовать глобальный механизм для предоставления коду доступа к нужному объекту, рассмотрим сначала вариант передачи им самого объекта. Это крайне просто и делает связность очевидной. И к том уже удовлетворяет все наши потребности.

Но... бывают ситуации, в которых ручная передача объекта ничем не обоснована или даже затрудняет чтение кода. Некоторые системы, такие как журналирование или управление памятью не должны быть частью модуля с публичным API. Параметры для вашего кода рендеринга должны касаться рендеринга, а не вещей типа журналирвоания.

Более того, остальные системы представляют системы, фундаментально независимые по своей сути. Ваша игра скорее всего может общаться только с одним аудио устройством и одной системой видеовывода. Это свойство окружающей среды, поэтому помещать их за десять слоев методов, чтобы к ним имел доступ только глубоко запрятанный метод не только бессмысленно, но и усложняет весь ваш код.

В подобных случаях этот шаблон может быть полезен. Как мы увидим дальше, он работает более гибким и настраиваемым образом, чем синглтон. При разумном использовании, он придаст вашей кодовой базе значительную гибкость без особых затрат в плане производительности.

Соответственно если применить шаблон ни к месту, он потянет за собой в качестве багажа весь шаблон Сигнлтон, да еще и с ущербом для производительности.

Имейте в виду

Основная сложность при работе с поиском службы заключается в том, что он берет зависимость - связь между двумя кусками кода - и откладывает их связывание до времени выполнения. Это добавляет нам гибкости, но ценой является усложнение понимания зависимостей при чтении кода.

Служба обязана находиться

Когда мы используем синглтон или статический класс, у нас не может возникнуть ситуации, когда объект недоступен. Вызывающий их код может быть уверен что он существует. Но так как этот шаблон предполагает поиск службы, нам нужно обрабатывать случаи когда поиск завершится неудачей. К счастью позже мы рассмотрим стратегию, направленную на то чтобы гарантировать наличие хоть какой-то службы, когда мы ее запрашиваем.

Служба не знает о том кто ее ищет

Так как поиск доступен глобально, любой код в игре может запрашивать службу и использовать ее. Это значит что служба должна уметь работать в любых обстоятельствах. Например, класс, который предполагается использовать во время части симуляции внутри игрового цикла, но не во время ренденринга не может быть службой - мы не можем гарантировать что он не будет использован в неподходящее время. Поэтому, если класс предполагается использовать только в определенном контексте, безопаснее избегать демонстрации его внешнему миру с помощью этого шаблона.

Пример кода

Вернемся к нашей проблеме с аудио системой и попробуем организовать ее видимость для остальной кодовой базы с помощью поиска службы.

Служба

Начнем с аудио API. Вот интерфейс, который будет предлагать наша служба:

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 консоли...
        }
};

Теперь у нас есть интерфейс и его реализация. Оставшаяся часть поиска службы - класс, связывающий их вместе.

Простой поиск

Следующая реализация представляет собой простейший тип поиска службы:

Такая техника обычно называется инъекцией зависимости (dependency injection) - неуклюжий жаргон для простой идеи. Предположим у вас есть класс, зависящий от другого. В нашем случае класс Locator нуждается в экземпляре службы Audio. Обычно поиск сам отвечает за его создание. Инъекция зависимости наоборот, предполагает что внешний код отвечает за инъекцию этой зависимости в объект, которому это нужно.

class Locator
{
    public:
        static Audio* getAudio() { return service_; }
        
        static void provide(Audio* service)
        {
            service_ = service;
        }
    
    private:
        static Audio* service_;
};

Статическая функция getAudio() выполняет поиск - мы можем вызвать ее откуда угодно из нашей кодовой базы и она вернет нам экземпляр Audio, который мы сможем использовать:

void someGameCode()
{
    Audio *audio = Locator::getAudio();
    audio->playSound(VERY_LOUD_BANG);
}

Способ "поиска" очень прост: он полагается на внешний код для регистрации поставщика службы, который должен быть выполнен прежде чем службой можно будет воспользоваться. Когда начинается игра, он вызывает нечто наподобие:

void initGame()
{
    ConsoleAudio *audio = new ConsoleAudio();
    Locator::provide(audio);
}

Главное, на что стоит обратить здесь внимание - это то что наша функция someGameCode() не заботится о конкретном классе ConsoleAudio, а требует только абстрактный интерфейс Audio. Не менее важно то, что даже класс поиска не привязан к конкретной предоставляемой службе. Единственное место в коде, которое знает о настоящем конкретном классе находится в функции инициализации, в которой служба регистрируется.

У нас есть еще один уровень снижения связности: интерфейс Audio не заботит тот факт, что доступ к нему обычно выполняется через поставщика службы. Все что он знает - это обычный абстрактный базовый класс. Это полезно, потому что означает что мы можем применять этот шаблон к существующим классам, которые не были специально для этого разработаны. В этом и заключается главная разница с синглтон (singleton)GoF, который непосредственно влияет на архитектуру класса "службы".

Нулевая служба

Пока что наша реализация довольно простая и достаточно гибкая. Но у нее есть одно серьезное ограничение: если мы попробуем использовать службу до того как она будет зарегистрирована, мы получим NULL. Если код вызова не сделает соответствующей проверки, наша игра упадет.

Я иногда слышу как это называют "временным связыванием (temporal coupling)": два разных куска кода, которые должны вызываться в определенной последовательности для корректной работы программы. В каждой программе присутствует нечто подобное, но как и в случае со всеми прочими видами связности, чем связности меньше, тем проще работать с кодовой базой.

К счастью существует еще один шаблон проектирования, называемый "Нулевой объект", который будет здесь уместен. Основная идея заключается в том что там где у нас возвращается 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. */ }
};

Как вы видите, он реализует интерфейс службы, но на самом деле ничего не делает. Теперь изменяем соответствующим образом наш поиск:

Вы можете заметить что теперь мы возвращаем службу по ссылке, а не через указатель. Так как ссылки в C++ (в теории!) никогда не могут быть равны NULL, возврат ссылки подсказывает пользователю кода что он всегда может рассчитывать на получение валидного объекта.

Еще одна вещь, на которую стоит обратить внимание - это то что мы делаем проверку на NULL в функции provide(), а не в accessor(). Это значит что нам нужно чтобы вызов initialize() выполнился раньше и поиск стал по умолчанию поставщиком нулевой службы. В свою очередь мы перемещаем туда ветвление из getAudio(), что в результате экономит нам несколько циклов при вызове.

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. Он гарантированно получит валидный объект.

Это очень полезно в случае если мы намеренно не находим службу. Если мы хотим временно отключить систему, у нас появился простой способ это сделать: мы просто ее не регистрируем в поставщике службы и поиск превратится в нулевого поставщика.

Отключение аудио может быть весьма полезным во время разработки. Так мы освобождаем немного памяти и процессорных циклов. И что более важно, это избавляет вас от прослушивания громкого скрежета, когда мы попадаем в отладчик во время проигрывания громкого звука. Нет ничего более бодрящего по утрам, чем зацикленный двадцатисекундный скрежет на полной громкости.

Декоратор журналирования

Теперь когда наша система превратилась в довольно удобную, давайте обсудим еще одно улучшение, которое может предложить нам этот шаблон: декорацию службы. Поясню на примере.

Во время разработки, ведение журнала произошедших интересных событий может существенно помочь в понимании того что происходит под капотом вашего игрового движка. Если вы работаете над AI, вам будет интересно узнать когда сущность изменяет состояние AI. Если вы звуковой программист, вас может интересовать список проигранных звуков чтобы вы могли проверить что они играются в правильном порядке.

Типичным решением является замусоривание кода вызовами функции log(). К сожалению это меняет одну проблему на другую: теперь у нас слишком много сообщений. Кодеру AI не очень интересно какой звук играется, а звуковику не очень интересны изменения состояния AI, но сейчас они оба вынуждены продираться через дебри предназначенных не им сообщений.

В идеале мы хотели бы иметь возможность включать журналирование только тех вещей, которые нас интересуют, а в финальной сборке игры журналирование вообще лишнее. Если для различных систем журналирование выглядит как служба, мы можем применить шаблон Декоратор (Decorator)GoF. Определим еще одну реализацию поставщика службы для аудио:

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)
        {
            // Код для журналирования сообщений...
        }
        
        Audio &wrapped_;
};

Как вы видите, он представляет собой обертку вокруг другого поставщика службы и предоставляет некий интерфейс. Он не только переправляет все вызовы настоящему поставщику службы, но и записывает в журнал каждый вызов. Если программисту захочется включить журналирование аудио, он может поступить так:

void enableAudioLogging()
{
    // Decorate the existing service.
    Audio *service = new LoggedAudio(Locator::getAudio());
    
    // Swap it in.
    Locator::provide(service);
}

Теперь любые вызовы к аудио службе оставят записи в журнале перед тем как сработать как и раньше. И конечно такое решение будет отлично работать с нашей нулевой службой, так что вы может выключить звук, но в журнал все равно будут поступать сообщения как будто он работает.

Архитектурные решения

Мы рассмотрели типичную реализацию, но у нас все же есть еще несколько вариантов реализации в зависимости от того как мы ответим на несколько ключевых вопросов.

Как выполняется поиск службы?

  • Ее регистрирует внешний код:

    Именно этот механизм использует наш пример для поиска службы и он же чаще всего применяется в играх.

    • Он быстрый и простой. Функция getAudio() просто возвращает указатель. Компилятор скорее всего превратит ее в inline и в результате у нас останется уровень абстракции и никакой потери в производительности.

    • Мы управляем созданием поставщика. Представьте себе службу для доступа к игровым контроллерам. У нас есть два конкретных поставщика: один для обычных игр и другой для сетевых. Сетевой поставщик передает ввод с контроллера по сети так что остальная игра и удаленные игроки видят его также как и локальный контроллер.

      Чтобы это заработало, конкретный сетевой поставщик должен знать IP адресс удаленного игрока. Если поиск сам создает объект, как он узнает что передать внутрь? Класс поиска не знает ничего о том, что такое сеть и еще меньше о том что такое IP адресс пользователя.

      Регистрация поставщика извне решает эту проблему. Вместо того чтобы класс создавал сам поиск, сетевой код игры создает ориентированный на работу с сетью поставщик и передает внутрь нужный IP адресс. Дальше он передает объект поиску, который знает только о существовании абстрактного интерфейса службы.

    • Мы можем изменить службу во время работы игры. В готовой игре это можно не использовать, но во время разработки это может пригодиться. Во время тестирования вы можете, например, заменить аудио службу на нулевую службу, о которой мы говорили выше и на время отключить звук прямо во время работы игры.

    • Поиск зависит от внешнего кода. Это конечно недостаток. Любой код, обращающийся к службе предполагает что кто-то где-то ее уже зарегистрировал. Если такой инициализации не было, у нас случится либо падение игры, либо произойдет нечто непредвиденное.

  • Привязка во время компиляции:

    Идея в том, что процесс "поиска" на самом деле происходит во время компиляции с помощью макросов препроцессора. Примерно так:

      class Locator
      {
          public:
              static Audio& getAudio() { return service_; }
      
          private:
              #if DEBUG
              static DebugAudio service_;
              #else
              static ReleaseAudio service_;
              #endif
      };

    Поиск службы в таком случае подразумевает несколько вещей:

    • Это быстро. Так как вся настоящая работа выполняется на этапе компиляции, во время выполнения ничего делать не нужно. Компилятор скорее всего сделает вызов getAudio() inline и в результате вызов получится максимально быстрым.

    • Вы можете гарантировать наличие службы. Так как поиск теперь обладает службой и выбирает его во время компиляции, мы можем предположить что если игра скомпилировалась, нам не нужно волноваться о недоступности службы.

    • Вы не можете так просто сменить службу. Это главный недостаток. Так как связывание происходит во время сборки, каждый раз когда вам захочется поменять службу, вам придется перекомпилировать игру.

  • Настройка во время работы:

    В скучных как цвет хаки землях бизнес приложений, если вы скажете "поиск службы (service locator)", будет подразумеваться именно это. Когда служба регистрируется, поиск занимается некоей магией во время работы чтобы найти нужную зарегистрированную реализацию.

    Обычно это означает загрузку файла настройки , которая описывает какую службу выбирать и затем выполняет отражение экземпляра этого класса во время выполнения. Этот процесс порождает следующие последствия.

    Отражение (Reflection) - это способность языка программирования работать с системой типов во время выполнения. Например, мы можем найти класс по его имени, найти его конструктор и вызвать его для создания экземпляра.

    Языки с динамической типизацией, такие как Lisp, Smalltalk и Python имеют такую способность от рождения, но даже новые статические языки, такие как C# или Java тоже их поддерживают.

    • Мы можем выполнять обмен служб без перекомпиляции. Это значительно более гибкий подход, чем связывание на этапе компиляции, но недостаточно гибкий как тот, который позволяет смену службы во время выполнения.

    • Службы могут изменять даже непрограммисты. Это полезно, когда дизайнер хочет запустить игру с некоторыми отключенными возможностями, но ради этого не хочется лезть в код игры. (Точнее говоря программистам не хочется этим заниматься).

    • Одна кодовая база может поддерживать несколько конфигураций одновременно. Так как поиск службы полностью убирается из кодовой базы, мы можем использовать один и тот же код для одновременной поддержки нескольких конфигураций служб.

      Это одна из причин почему эта модель преобладает в промышленной web-разработке: вы можете разместить одно и то же приложение на разных серверах, лишь немного изменив файл настройки. В играх это не так полезно. Консольное железо обычно стандартизировано и даже PC игры создаются в расчете на определенную конфигурацию.

    • Это сложно. В отличие от предыдущего решения, это довольно тяжеловесное. Вы можете создать систему настройки, возможно написать код для загрузки и парсинга файла и даже предпринять какие-то меры для локализации службы. Но только вот время, потраченное на все это - это время, не потраченное на саму игру.

    • Поиск службы требует времени. И теперь наша улыбка окончательно исчезнет. Переход к конфигурированию во врем выполнения означает, что нам придется тратить на это циклы процессора для поиска службы. Это можно минимизировать с помощью кэширования, но при первом использовании службы игра все равно будет тратить время на ее поиск. Игровые разработчики ненавидят тратить циклы процессора на вещи, не улучшающие впечатления игрока от игрового процесса.

Что произойдет если служба не будет найдена?

  • Пусть это обрабатывает пользователь:

    Такое решение проще чем потратить доллар. Если поиск не может найти службу, он возвращает NULL. Сюда входит:

    • Он позволяет пользователю определить как обрабатывать ошибку. Некоторые пользователи могут расценивать невозможность найти службу как критическую ошибку, которая должна привести к остановке игры. Другие могут ее проигнорировать и продолжить работу. Если поиск не может определить общую политику для всех случаев, то передача ошибки вниз по дереву наследования позволит каждому решить как на нее реагировать самостоятельно.

    • Пользователи службы должны обрабатывать ошибку. Конечно из этого следует и то что каждое место вызова должно выполнять проверку найдена ли служба. Если почти все они будут обрабатывать ошибку одинаково, мы получим очень много дублирующего кода по всей кодовой базе. И если хотя бы в одном из этих мест мы забудем вставить проверку, игра может упасть.

  • Остановка игры:

    Я упоминал, что мы можем утверждать (prove) что служба всегда будет доступна во время компиляции, но это еще не значит что мы можем заявлять (declare) что его наличие является частью соглашения с поиском во время выполнения. Проще всего это сделать с помощью assert:

    Функция утверждения assert - это способ внедрить такое соглашение в код. Когда вызывается assert(), она вычисляет переданное ей выражение. Если результат равен true, то ничего не происходит и игра продолжается. А вот если результат равен false, игра немедленно останавливается. В отладочной сборке при этом обычно запускается отладчик или по крайней мере выводится сообщение с именем файла и номером строки, где сработал assert.

    assert() означает следующее: "Я утверждаю что это выражение всегда справедливо. А если нет - значит это баг и я прерываю выполнение немедленно чтобы вы его поправили." Таким образом вы заключаете соглашение с областью кода. Если функция утверждения обнаруживает что один из аргументов не равен NULL, он говорит "Контракт между мной и кодом вызова утверждает что мне не будет передан NULL".

    Утверждение помогает отследить баги как только они в игре возникнут, а не потом, когда ошибка приведет к видимому пользователю некорректному поведению. Это ограда вокруг кодовой базы, ограничивающая баги от того чтобы они разбегались по всему коду, который их породил.

      class Locator
      {
          public:
              static Audio& getAudio()
              {
                  Audio* service = NULL;
              
                  // Здесь находится код для поиска службы...
              
                  assert(service != NULL);
                  return *service;
              }
      };

    Если служба не найдена, игра останавливается перед тем как ее попробует использовать следующий код. Вызов assert() не решает здесь проблему ненайденной службы, но зато показывается где произошла ошибка. Мы как будто утверждаем "Невозможность найти службу является багом поиска".

    Так что же это нам дает?

    • Пользователю не нужно обрабатывать ненайденную службу. Так как одна и та же служба может использоваться в сотне мест, нам нужно написать гораздо меньше кода. Утверждая что поиск всегда возвращает службу, мы ограждаем пользователя от необходимости этим заниматься.

    • Игра останавливается если служба не найдена. Если служба действительно не будет найдена, игра остановится. Это хорошо, потому что заставит нас обратить внимание на баг, не дающий службе найтись (если например код инициализации не был вызван там где следует), но это будет тормозить всех остальных до тех пор пока ошибка не будет исправлена. Для большой команды разработчиков это может вылиться в длительный перерыв в работе.

  • Возвращение нулевой службы:

    Мы показали такое усовершенствование в примере реализации. Его использование означает что:

    • Пользователям не нужно обрабатывать пропавшую службу. Как и в предыдущем случае, мы проверяем чтобы у нас всегда возвращалась валидная служба, упрощая код, использующий службу.

    • Игра будет продолжать работать если служба не доступна. Это одновременно и зло и благо. Это будет полезно если мы хотим чтобы игра работала, даже когда служба недоступна. Например это полезно при работе большой командой когда функционал, над которым мы работаем зависит от другой системы, которая еще не готова.

      Недостаток заключается в том что нам будет сложнее отлаживать случайно ненайденную службу. Предположим игра использует службы для доступа к данным и затем на основе этих данных принимает решение. Если мы не смогли зарегистрировать настоящую службу и код вместо нее получит нулевую службу, игра не будет вести себя так как мы ожидаем. И понадобится немало работы чтобы отследить ошибку, заключающуюся в том что служба была недоступна когда нам понадобится.

      Мы можем облегчить свою участь, если поместим в нулевую службу отладочное сообщение, выводимое при ее использовании.

Среди всех этих вариантов, я чаще всего наблюдаю простое утверждение (asserting) того что служба найдена. К тому времени как игра выходит, ее тестируют на самых разнообразных конфигурациях. Шанс того что служба после этого не будет найдена крайне мал.

В большой команде я советую вам воспользоваться нулевой службой. Ее не сложно реализовать и она убережет вас от простоя, когда служба недоступна. А еще вы легко сможете отключить службу на время если она содержит ошибки или просто вам мешает.

Какова область видимости службы?

До сих пор мы предполагали, что поиск предоставляет доступ к службе всем, кому это нужно. Несмотря на то что это самый распространенный способ применения шаблона, есть и еще один вариант ограничения доступа к самому классу и его наследникам:

class Base
{
    // Код для поиска службы и установки service_...
    
    protected:
        // Классы наследники могут использовать службу
        static Audio& getAudio() { return *service_; }
    
    private:
        static Audio* service_;
};

Таким образом доступ к службе ограничивается классами, унаследованными от Base. Мы в любом случае в выигрыше:

  • Если доступ глобальный:

    • Вся кодовая база имеет возможность воспользоваться службой. Большая часть служб должна существовать в едином экземпляре. Позволив всей кодовой базе получать доступ к службе, мы можем избежать появления экземпляров поставщиков по всей кодовой базе, потому что они не смогут обратиться к "настоящему".

    • Мы утрачиваем контроль над тем где используется служба. Это очевидная плата за объявление чего либо глобальным: кто угодно может к ней обратиться. В главе синглтон (singleton)GoF полно страшных историй о том к чему приводит глобальность.

  • Если доступ ограничен конкретным классом:

    • Мы управляем связностью. Это главное преимущество. Ограничивая доступ к службе до ветви дерева иерархи, мы позволяем несвязанным системам оставаться несвязанными.

    • Это может привести к дублированию усилий. Потенциальный недостаток заключается в том что если нескольким сторонним классам потребуется доступ к службе, им придется устанавливать отдельную с ней связь. Какой бы процесс для поиска или регистрации службы не использовался, в этих классах будет иметь место дублирование.

      (Еще одна альтернатива заключается в изменении иерархии класса, так что бы сосредоточить все это в базовом классе, но такие сложности не обязательно стоят затраченных на них усилий.)

Моя общая рекомендация заключается в том, что если служба ограниченна в игре какой-то одной областью, область ее видимости стоит этим классом и ограничить. Например, если служба получения доступа к сети может быть ограничена сетевыми классами. Службы, используемые в более широком смысле, такие как журналирование, могут быть и глобальными.

Смотрите также

  • Шаблон Поиск службы во многом является родственником синглтона (singleton)GoF, так что вам стоит рассмотреть оба варианта и выбрать лучший.

  • Фреймворк Unity использует этот шаблон вместе с шаблоном Компонент (Component) в методе GetComponent().

  • Фреймворк Microsoft’s XNA содержит этот шаблон внутри своего главного класса Game. У каждого экземпляра есть объект GameServices , который можно использовать для регистрации и поиска любых типов служб.

Last updated