Готовый перевод 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
Настройки
Готово:
