Как стать автором
Обновить
817.66
OTUS
Цифровые навыки от ведущих экспертов

Service Locator — антипаттерн

Время на прочтение6 мин
Количество просмотров22K
Автор оригинала: blog.ploeh

Service Locator (или “локатор служб”) — хорошо всем нам известный паттерн. Поскольку он был описан Мартином Фаулером, он должен быть хорошим, ведь так?

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

Давайте разберемся, что с ним не так. Если отвечать коротко, то проблема с Service Locator заключается в том, что он скрывает зависимости класса, что вызывает ошибки времени выполнения вместо ошибок времени компиляции, а также усложняет сопровождение кода, потому что становится не совсем ясно, какие изменения окажутся критическими.

Пример с OrderProcessor

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

public class OrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        var validator = Locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper = Locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}

Service Locator используется в качестве альтернативы оператору new. Выглядит это следующим образом:

public static class Locator
{
    private readonly static Dictionary<Type, Func<object>>
        services = new Dictionary<Type, Func<object>>();
 
    public static void Register<T>(Func<T> resolver)
    {
        Locator.services[typeof(T)] = () => resolver();
    }
 
    public static T Resolve<T>()
    {
        return (T)Locator.services[typeof(T)]();
    }
 
    public static void Reset()
    {
        Locator.services.Clear();
    }
}

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

Он гибок и расширяем, и даже поддерживает подмену сервисов тестовыми дублерами, как мы вскоре увидим.

Учитывая все это, в чем же подвох?

Проблемы с API

Предположим на мгновение, что мы просто являемся пользователями класса OrderProcessor. Сами мы его не писали, а получили в сборке от третьих лиц, и нам еще даже не довелось взглянуть на него в Reflector.

Вот что мы получаем от IntelliSense в Visual Studio:

Итак, у класса есть конструктор по умолчанию. Это означает, что мы можем просто создать его новый инстанс и сразу же вызвать метод Process:

var order = new Order();
var sut = new OrderProcessor();
sut.Process(order);

Но не тут-то было, запуск этого кода внезапно вызывает KeyNotFoundException, потому что IOrderValidator не был зарегистрирован нами в Locator. Это не только неожиданно, это вполне может сбить нас с толку, если у нас нет доступа к исходному коду.

Просматривая исходный код (или используя Reflector) или копаясь в документации (фе!) мы можем, наконец, обнаружить, что нам нужно зарегистрировать инстанс IOrderValidator в Locator (совершенно несвязанным статическим классом), чтобы это все заработало.

В модульном тесте это можно сделать так:

var validatorStub = new Mock<IOrderValidator>();
validatorStub.Setup(v => v.Validate(order)).Returns(false);
Locator.Register(() => validatorStub.Object);

Что еще более раздражает, так это то, что, поскольку внутреннее хранилище Locator статично, нам нужно вызывать метод Reset после каждого модульного теста. Но хотя бы это неудобство нас преследует в основном только в модульном тестировании.

В целом, однако, мы не можем обоснованно утверждать, что такое API способствует положительному опыту разработки.

Проблемы с сопровождением

Хоть подобное использование Service Locator с точки зрения пользователя достаточно проблематично, но то, что кажется простым, вскоре становится проблемой и для разработчика, который должен сопровождать этот код.

Допустим, нам нужно расширить поведение OrderProcessor вызовом метода IOrderCollector.Collect. Легко ли нам будет сделать это?

public void Process(Order order)
{
    var validator = Locator.Resolve<IOrderValidator>();
    if (validator.Validate(order))
    {
        var collector = Locator.Resolve<IOrderCollector>();
        collector.Collect(order);
        var shipper = Locator.Resolve<IOrderShipper>();
        shipper.Ship(order);
    }
}

С чисто механистической точки зрения это легко — мы просто добавили новый вызов Locator.Resolve и вызвали IOrderCollector.Collect.

Является ли это изменение критическим?

Ответить на этот вопрос может быть на удивление трудно. Код, конечно, скомпилировался без ошибок, но один из моих модульных тестов демонстрирует ошибку. Что произойдет в рабочем приложении? Интерфейс IOrderCollector может быть уже зарегистрирован в Service Locator, поскольку он уже используется другими компонентами, и в этом случае он будет работать без сбоев. С другой стороны, дела могут обстоять немного по другому.

Суть в том, что становится намного сложнее сказать, вводите ли вы критическое изменение или нет. Вам нужно следить за всем приложением, в котором используется Service Locator, и компилятор вам в этом не поможет.

Вариация паттерна: Concrete Service Locator

Можем ли мы как-то решить эти проблемы?

Одно из самых популярных решений — сделать Service Locator конкретным классом, который используется следующим образом:

public void Process(Order order)
{
    var locator = new Locator();
    var validator = locator.Resolve<IOrderValidator>();
    if (validator.Validate(order))
    {
        var shipper = locator.Resolve<IOrderShipper>();
        shipper.Ship(order);
    }
}

Однако для работы ему все еще требуется статическое хранилище в памяти:

public class Locator
{
    private readonly static Dictionary<Type, Func<object>>
        services = new Dictionary<Type, Func<object>>();
 
    public static void Register<T>(Func<T> resolver)
    {
        Locator.services[typeof(T)] = () => resolver();
    }
 
    public T Resolve<T>()
    {
        return (T)Locator.services[typeof(T)]();
    }
 
    public static void Reset()
    {
        Locator.services.Clear();
    }
}

Другими словами: нет никаких структурных различий между конкретным локатором служб и статическим локатором служб, который мы уже рассматривали. Он имеет те же проблемы и ничего не решает.

Еще одна вариация: Abstract Service Locator #

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

public interface IServiceLocator
{
    T Resolve<T>();
}
 
public class Locator : IServiceLocator
{
    private readonly Dictionary<Type, Func<object>> services;
 
    public Locator()
    {
        this.services = new Dictionary<Type, Func<object>>();
    }
 
    public void Register<T>(Func<T> resolver)
    {
        this.services[typeof(T)] = () => resolver();
    }
 
    public T Resolve<T>()
    {
        return (T)this.services[typeof(T)]();
    }
}

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

public class OrderProcessor : IOrderProcessor
{
    private readonly IServiceLocator locator;
 
    public OrderProcessor(IServiceLocator locator)
    {
        if (locator == null)
        {
            throw new ArgumentNullException("locator");
        }
 
        this.locator = locator;
    }
 
    public void Process(Order order)
    {
        var validator =
            this.locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper =
                this.locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}

Значит, теперь все хорошо?

Как разработчики, мы наконец получили небольшую помощь от IntelliSense:

Но что это нам говорит? По большому счету, не очень много чего. Итак, OrderProcessor нужен ServiceLocator — это немного больше информации, чем раньше, но мы по-прежнему не знаем, какие службы необходимы. Следующий код компилируется, но вылетает с тем же KeyNotFoundException, что и раньше:

var order = new Order();
var locator = new Locator();
var sut = new OrderProcessor(locator);
sut.Process(order);

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

Заключение

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

Компилятор может предложить как пользователям, так и поставщикам немного помощи при внедрении зависимостей через конструктор, но эта помощь недоступна для API, которые полагаются на Service Locator.

Подробнее о паттернах и антипаттернах внедрения зависимостей можно прочитать в моей книге.

Обновление от 20 мая 2014 г.: Еще один способ объяснить отрицательные аспекты Service Locator заключается в том, что он нарушает SOLID.

Обновление от 26 октября 2015 г.: Фундаментальная проблема с Service Locator заключается в том, что он нарушает инкапсуляцию.


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

Недавно в OTUS в рамках онлайн-курса "Unity Game Developer. Professional" прошел открытый урок «Компоненты игровых объектов». На уроке мы рассмотрели, как при помощи компонентного подхода (не ECS) можно гибко изменять функциональность юнитов таким образом, чтобы система была слабо связной. Если интересно, запись вебинара можно посмотреть по ссылке.

Теги:
Хабы:
Всего голосов 17: ↑9 и ↓8+1
Комментарии17

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS