Tuesday, October 20, 2015

ServiceLocator with top MVVM Frameworks

Здравствуйте, уважаемые читатели. Сегодня мы поговорим о неоднозначном паттерне, который называется Service Locator (Локатор служб). К этому паттену можно относиться по-разному. Его часто считают анти-паттерном, как, например, Марк Симан в своей книге Dependency Injection in .NET. В этой же книге он наводит много аргументов в пользу того, что данный паттерн можно считать анти-паттерном из-за того, что количество недостатков превышает его преимущества. Другие считают его полноценным паттерном (в том числе, и я из-за его простоты и возможности использования). Как вы для себя определили, Service Locator как паттерн или анти-паттерн является исключительно по вашему предпочтению. Есть замечательная статья Сергея Теплякова об этом паттерне, которая называется  "DI Паттерны. Service Locator" , которой я бы поставил оценку 5 из 5-ти и рекомендовал бы к прочтению, так как подход автора и мысли во многом совпадают с моими. Поэтому постараюсь объяснить причину, почему я решил писать свою статью по данному паттерну. Я решил, в отличие от тех, которые либо хвалят, либо осуждают Service Locator, поделиться с вами тем, как его использовать и какие самые популярные варианты Service Locator вы можете встретить. Вероятнее всего, здесь будут указано, что некоторые части плохие или нет, но это больше можно назвать как sharing knowledge (поделиться знаниями), чем попытка вас в чем-либо переубедить. Считаю, что эта статья может быть полезна как с точки зрения использования, так и с точки зрения того, что вы можете знать где и какой Service Locator готовый можно найти. Начнем, пожалуй, с реализации.
Custom realization
Начнем, пожалуй, с теории. Существует два способа реализации Service Locator – через статический класс и через интерфейс. Мы будем сегодня рассматривать вперемешку все реализации и начнем, пожалуй, с самой простой с использованием статического класса. Вы должны получить возможность зарегистрировать ваш новый тип данных, иметь возможность получить его с помощью вашего сервиса, а также иметь возможность почистить все данные. Последнюю возможность зачастую не реализуют. Ниже приведен пример самой простой реализации данного паттерна.
public static class ServiceLocator
{
    private static readonly Dictionary<Typeobject> Services = new Dictionary<Typeobject>();

    public static void RegisterService(Type serviceType, object service)
    {
        if (Services.ContainsKey(serviceType))
        {
            Services[serviceType] = service;
        }
        else
        {
            Services.Add(serviceType, service);
        }
    }

    public static object GetService(Type serviceType)
    {
        if (!Services.ContainsKey(serviceType))
            return null;

        return Services[serviceType];
    }

    public static void Reset()
    {
        Services.Clear();
    }
}
Но как вы понимаете, в реальном проекте этот метод использовать неудобно. Одно только приведение с object к вашему типу чего стоит. Есть вариант дописать еще один метод Get или GetService, который будет возвращать дженерик вариант использования.
public static T Get<T>() where T : class
{
    Type serviceType = typeof(T);

    Debug.Assert(!Services.ContainsKey(serviceType) || Services[serviceType] is T);

    return (T)GetService(serviceType);
}
Либо адаптировать вариант под практическое использование.
public static class ServiceLocator
{
    public static void Register<T>(T service) where T : class
    {
        if (service == null)
            Unregister<T>();

        RegisterService(typeof(T), service);
    }
     
    public static void RegisterIfNew<T>(T service) where T : class
    {
        if (service == null)
            Unregister<T>();

        if (Services.ContainsKey(typeof(T)))
            return;

        RegisterService(typeof(T), service);
    }

    public static void Unregister<T>() where T : class
    {
        UnregisterService(typeof(T));
    }

    public static T Get<T>() where T : class
    {
        Type serviceType = typeof(T);

        Debug.Assert(!Services.ContainsKey(serviceType) || Services[serviceType] is T);

        return (T)GetService(serviceType);
    }

    public static void UnregisterAll()
    {
        lock (Services)
        {
            Services.Clear();
        }
    }


    #region Implementation

    private static readonly Dictionary<Typeobject> Services = new Dictionary<Typeobject>();


    private static void RegisterService(Type serviceType, object service)
    {
        lock (Services)
        {
            if (Services.ContainsKey(serviceType))
            {
                Services[serviceType] = service;
            }
            else
            {
                Services.Add(serviceType, service);
            }
        }
    }

    private static void UnregisterService(Type serviceType)
    {
        lock (Services)
        {
            if (!Services.ContainsKey(serviceType))
                return;

            Services.Remove(serviceType);
        }
    }

    private static object GetService(Type serviceType)
    {
        lock (Services)
        {
            if (!Services.ContainsKey(serviceType))
                return null;

            return Services[serviceType];
        }
    }

    #endregion

}
Теперь использовать его в коде можно следующим образом.
class Program
{
    public interface ICustomer { }
    public class Customer : ICustomer { }

    static void Main(string[] args)
    {
        ServiceLocator.RegisterIfNew<ICustomer>(new Customer());

        var customer = ServiceLocator.Get<ICustomer>();

        Console.ReadLine();
    }
}
В дебаге, если посмотреть, можно увидеть результат того, что все работает.
Embedded .Net Implementation
Мало кто знает, но в .NET, начиная чуть ли ни с первой версии, есть свой сервис локатор, который называется ServiceContainer. Признаться честно, я сам не так давно об этом узнал.
class Program
{
    public interface ICustomer { }
    public class Customer : ICustomer { }

    static void Main(string[] args)
    {
        var serviceContainer = new ServiceContainer();
        serviceContainer.AddService(typeof(ICustomer), new Customer());

        var customer = serviceContainer.GetService(typeof(ICustomer));

        Console.ReadLine();
    }
}
Этот сервис локатор используется в самом .NET Framework. Возможно, причина в том, что его просто решили не убирать, чтобы сохранить обратную совместимость.
MVVM Light
Теперь пора пройтись по популярным нынче MVVM фреймворкам. Так как на первом месте стоит MVVM Light, начнем обзор, пожалуй, с него. В MVVM Light есть простой сервис локатор, который называется SimpleIoc, который зачастую используют с другим классом ServiceLocator (библиотека Microsoft.Practices.ServiceLocation), который позволяет вам получить зарегистрированные ваши типы данных для использования. Давайте рассмотрим пример.
public class ViewModelLocator
{
    public ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        SimpleIoc.Default.Register<Program.ICustomerProgram.Customer>();
        SimpleIoc.Default.Register<MainViewModel>();
    }

    public MainViewModel Main
    {
        get
        {
            return ServiceLocator.Current.GetInstance<MainViewModel>();
        }
    }
        
    public static void Cleanup()
    {
        // TODO Clear the ViewModels
    }
}
Выше приведен пример реализации класса ViewModelLocator, который связывает наши интерфейсы с реализацией. А теперь само использование:
class Program
{
    public interface ICustomer { }
    public class Customer : ICustomer { }

    static void Main(string[] args)
    {
        var viewModelLocator = new ViewModelLocator();
        var main = viewModelLocator.Main;

        var customer = ServiceLocator.Current.GetInstance<ICustomer>();

        Console.ReadLine();
    }
}
Можете запустить в дебаг-режиме ваш проект и посмотреть, что все работает. На примере с подходом. который использует MVVM Light, хотелось бы остановится подробнее. Для того чтобы вместо SimpleIoc использовать другую реализацию, например, существующего полноценного IoC контейнера, нам нужно написать адаптер, который будет реализовать интерфейс IServiceLocator. Например, вот так (это кастомная реализация, которая очень упрощенная. В Unity есть готовый UnityServiceLocator, который вы можете использовать для своих нужд).
public class UnityServiceLocator : IServiceLocator
{
    private readonly IUnityContainer _container;
    public UnityServiceLocator(IUnityContainer unityContainer)
    {
        _container = unityContainer;
    }

    public object GetService(Type serviceType)
    {
        return _container.Resolve(serviceType);
    }

    public object GetInstance(Type serviceType)
    {
        return _container.Resolve(serviceType);
    }

    public object GetInstance(Type serviceType, string key)
    {
        return _container.Resolve(serviceType, key);
    }

    public IEnumerable<object> GetAllInstances(Type serviceType)
    {
        return _container.ResolveAll(serviceType);
    }

    public TService GetInstance<TService>()
    {
        return _container.Resolve<TService>();
    }

    public TService GetInstance<TService>(string key)
    {
        return _container.Resolve<TService>(key);
    }

    public IEnumerable<TService> GetAllInstances<TService>()
    {
        return _container.ResolveAll<TService>();
    }
}
Использовать это все можно как-то так:
public class ViewModelLocator
{
    private readonly IUnityContainer _container;
    public ViewModelLocator()
    {
        _container = new UnityContainer();
        ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(_container));

        _container.RegisterType<Program.ICustomerProgram.Customer>();
        _container.RegisterType<MainViewModel>();
    }

    public MainViewModel Main
    {
        get
        {
            return ServiceLocator.Current.GetInstance<MainViewModel>();
        }
    }
        
    public static void Cleanup()
    {
        // TODO Clear the ViewModels
    }
}
В принципе, это все что хотелось бы рассказать об SimpeIoc как сервис локаторе, как зачастую его используют и чем можно это заменить, и пора начинать рассмотрение других примеров сервис локаторов.
Caliburn Micro
В Caliburn Micro тоже есть свой сервис локатор, который называется IoC. Он работает в связке с простым IoC контейнером, который идет в поставке и называется SimpleContainer. Ниже приведен пример использования сервиса локатор Caliburn Micro на практике. Ниже приведена реализация загрузчика, который инициализируется при старте вашего приложения.
public class CustomBootstrapper : BootstrapperBase
{
    public CustomBootstrapper()
    {
        Initialize();
    }

    protected override void OnStartup(object sender, StartupEventArgs e)
    {
        DisplayRootViewFor<MainViewModel>();
    }

    private readonly SimpleContainer _container = new SimpleContainer();

    protected override object GetInstance(Type service, string key)
    {
        return _container.GetInstance(service, key);
    }

    protected override IEnumerable<object> GetAllInstances(Type service)
    {
        return _container.GetAllInstances(service);
    }

    protected override void BuildUp(object instance)
    {
        _container.BuildUp(instance);
    }

    protected override void Configure()
    {
        _container.PerRequest<Program.ICustomerProgram.Customer>();

        IoC.GetInstance = _container.GetInstance;
        IoC.GetAllInstances = _container.GetAllInstances;
        IoC.BuildUp = _container.BuildUp;
    }
}
Обратите внимание, как инициализируется IoC в Caliburn Micro (метод Configure), для того чтобы нормально возвращать нужные типы данных. Использовать это можно следующим образом.
class Program
{
    public interface ICustomer { }
    public class Customer : ICustomer { }

    static void Main(string[] args)
    {
        var bootstrapper = new CustomBootstrapper();
        var customer = IoC.Get<ICustomer>();

        Console.ReadLine();
    }
}
Чтобы убедиться, что все работает, запустим в дебаг-режиме наш код.
Catel
Фреймворк Catel также реализует свой сервис локатор. Я специально пропустил Prism, поскольку они используют один и тот же сервис локатор. Поэтому не было особого смысла писать дублирование кода. Пример с использованием сервис локатора для Catel является самым простым из приведенных выше.
using Catel.IoC;

namespace ServiceLocatorSample
{
    class Program
    {
        public interface ICustomer { }
        public class Customer : ICustomer { }

        static void Main(string[] args)
        {
            ServiceLocator.Default.RegisterType<ICustomerCustomer>();
            var customer = ServiceLocator.Default.ResolveType<ICustomer>();

            Console.ReadLine();
        }
    }
}
Правда, внутри он реализован намного сложнее, чем все приведенные выше. Внутри данный ServiceLocator использует класс TypeFactory, для того чтобы работать корректно с типами данных. Мы не будем глубоко копать в реализацию сервис локатора в Catel, так как вы можете это сделать по желанию сами.

Итоги
Сегодня мы рассмотрели несколько вариантов использования паттерна Service Locator, начиная с ручной реализации и заканчивая реализациями, предлагаемыми в популярных MVVM фреймворках. Как вы увидели,каждый разработчик использует данный паттерн по-разному. В Mvvm Light он идет в виде статического класса SimpleIoc, и мы рассмотрели его в комбинации с ServiceLocator, который предлагает Microsoft. Этот же сервис локатор используется в Prism (в примерах, которые идут к призму, вы можете в этом убедиться). Caliburn Micro использует также реализацию в виде статического класса IoC со своей реализацией IoC контейнера. В отличие от предыдущих MVVM фреймворков, Catel предпочел реализацию в виде интерфейса. В целом, решать использовать или нет данный паттерн – сугубо ваше решение, но как вы видите, множество готовых фреймворков используют и реализуют его. Популярные варианты мы с вами рассмотрели. Надеюсь, они вам пригодятся.