Готовый перевод Game Programming Patterns / Паттерны программирования в играх: 4.3 Service Locator

Цель

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

Причина

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

Достаточно трудно найти ту часть игры, которая не нуждается в распределении памяти, логировании, файловой системе или случайных числах.

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

К примеру, возьмем аудио.

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

Снайпер стреляет из своего оружия и раздается звук выстрела (ИИ).

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

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

// Использовать статический класс?

AudioSystem::playSound(VERY_LOUD_BANG)

;

// А может Singleton?

AudioSystem::instance()->playSound(VERY_LOUD_BANG)

;

Каждый способ приводит к цели, но по пути мы намертво к нему привязваемся.

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

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

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

Есть решение получше: телефонная книга.

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

При переезде мы уведомляем об этом телефонную компанию.

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

Вообще-то, мы даже совсем не обязаны предоставлять свой реальный адрес.

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

Так работает паттерн Service Locator: он отделяет код, которому требуется служба, и от того что это за служба (от конкретной реализации типа),

и от того, где она расположена (как мы получаем ее экземпляр).

Класс службы определяет абстрактный интерфейс к набору операций.

Конкретный поставщик службы реализует этот интерфейс.

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

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

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

Это основная проблема паттерна Singleton, и этот паттерн не исключение.

Мой простейший совет о том, как использовать Service Locator - используйте экономно.

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

Просто как 5 копеек, и делает связывание очевидным.

Охватывает большую часть ваших ситуаций.

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

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

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

Ваша игра, скорее всего, взаимодействует лишь с одним аудио устройством или видео системой.

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

В таких случаях, этот паттерн может помочь.

Техника, примененная тут, называется внедрением зависимостей (Dependency Injection), неуклюжий жаргон для весьма простой идеи.

Скажем, есть класс, зависимый от другого.

В нашем случае, нашему классу Locator нужен экземпляр службы Audio.

Обычно, локатор сам отвечает за создание такого экземпляра.

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

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

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

При запуске игра вызывает примерно такой код:

Здесь ключевым моментом является то, что наша функция someGameCode() не в курсе конкретной реализации класса ConsoleAudio, а только его абстрактного интерфейса Audio.

Не менее важно и то, что класс локатора также не связан с конкретным поставщиком службы.

Единственно место в коде, которое знает о конкретном классе - это функция инициализации, регистрирующая службу.

Тут дополнительный уровень декомпозиции: интерфейс Audio не в курсе того, что он используется в целой куче мест через Service Locator.

Он знает лишь, лишь что он - просто обычный абстрактный базовый класс.

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

В этом его отличие от Singleton, который влияет на дизайн самого класса службы.

Null-сервис

Пока что наша реализация одновременно и проста и довольно гибка.

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

И если вызывающий код это не проверит, то будет сбой игры.

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

Переведено на Нотабеноиде
http://notabenoid.org/book/51192/215228

Переводчики: Deo, bubenok

Настройки

Готово:

14.77% КП = 1.0

Непереведённые фрагменты будут пропущены.