Реализуем сами простой IoC контейнер

    • .NET
    • C#
    • Patterns
    • IoC
    • Unity
  • modified:
  • reading: 5 minutes

Думаю, что даже уже начинающий разработчик должен быть знаком с понятием Inversion of Control (сокращают как IoC). Любой проект сейчас начинается с выбора фреймворка, при помощи которого будет реализован принцип внедрения зависимостей. Если взять русскую википедию, то там определение для IoC выглядит следующим образом:

Инверсия управления (Inversion of Control, IoC) — важный принцип объектно-ориентированного программирования, используемый для уменьшения связанности в компьютерных программах и входящий в пятерку важнейших принципов SOLID.

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

Для .NET платформы, как и для любых других платформ, есть огромное разнообразие библиотек, которые можно использовать в проектах: Unity, StructureMap, Ninject, Castle Windsor. Это только часть, которую я вспомнил на данный момент, но есть и еще немалое количество, помню даже кто-то из знакомых писал свой. Для бизнес проектов, ну и для проектов, бинарники которых вижу только я, мне хватает этих библиотек, да более того мне хватает только Unity. Но вот, если хочется написать какую-нибудь утилиту или приложение для общественности, либо библиотеку, то написав приложение в 100 килобайт тянут за ним еще по 300 килобайт библиотеки для записи логов и 300 библиотеки, реализующей для тебя IoC немного дико. И дико иметь привязку на какую-то специфичную реализацию IoC, особенно, если вы распространяете библиотеку, ведь ваши пользователи могут держать в привычке использовать совершенно другую реализации IoC. А в случае приложения дело даже не в размере, а в том, что у вас вместо всего одного exe файла будет поставляться еще гора каких-то непонятных библиотек (все зависит конечно еще от того, как будете распространять свое приложение). Есть, конечно, еще и простое решение, можно объединить все ваши бинарники приложения при помощи утилиты ILMerge.exe в один exe файл. Ну а все-таки, если дело в размере? Хочется, чтобы приложение было действительно очень небольшим в размерах.

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

public class IoC
{    private readonly IDictionary<Type, RegisteredObject> _registeredObjects = new Dictionary<Type, RegisteredObject>();
     public void Register<TType>() where TType : class
    {        Register<TType, TType>(false, null);
    }
     public void Register<TType, TConcrete>() where TConcrete : class, TType 
    {        Register<TType, TConcrete>(false, null);
    }
     public void RegisterSingleton<TType>() where TType : class
    {
        RegisterSingleton<TType, TType>();
    }
     public void RegisterSingleton<TType, TConcrete>() where TConcrete : class, TType 
    {        Register<TType, TConcrete>(true, null);
    }
     public void RegisterInstance<TType>(TType instance) where TType : class
    {
        RegisterInstance<TType, TType>(instance);
    }
     public void RegisterInstance<TType, TConcrete>(TConcrete instance) where TConcrete : class, TType 
    {        Register<TType, TConcrete>(true, instance);
    }
     public TTypeToResolve Resolve<TTypeToResolve>()
    {        return (TTypeToResolve)ResolveObject(typeof(TTypeToResolve));
    }
     public object Resolve(Type type)
    {        return ResolveObject(type);
    }
     private void Register<TType, TConcrete>(bool isSingleton, TConcrete instance)
    {        Type type = typeof(TType);
        if (_registeredObjects.ContainsKey(type))
            _registeredObjects.Remove(type);        _registeredObjects.Add(type, new RegisteredObject(typeof(TConcrete), isSingleton, instance));
    }
     private object ResolveObject(Type type)
    {        var registeredObject = _registeredObjects[type];
        if (registeredObject == null)
        {            throw new ArgumentOutOfRangeException(string.Format("The type {0} has not been registered", type.Name));
        }        return GetInstance(registeredObject);
    }
     private object GetInstance(RegisteredObject registeredObject)
    {        object instance = registeredObject.SingletonInstance;
        if (instance == null)
        {            var parameters = ResolveConstructorParameters(registeredObject);
            instance = registeredObject.CreateInstance(parameters.ToArray());
        }        return instance;
    }
     private IEnumerable<object> ResolveConstructorParameters(RegisteredObject registeredObject)
    {        var constructorInfo = registeredObject.ConcreteType.GetConstructors().First();
        return constructorInfo.GetParameters().Select(parameter => ResolveObject(parameter.ParameterType));
    }
     private class RegisteredObject
    {        private readonly bool _isSinglton;
         public RegisteredObject(Type concreteType, bool isSingleton, object instance)
        {
            _isSinglton = isSingleton;
            ConcreteType = concreteType;
            SingletonInstance = instance;
        }
         public Type ConcreteType { get; private set; }
         public object SingletonInstance { get; private set; }
         public object CreateInstance(params object[] args)
        {            object instance = Activator.CreateInstance(ConcreteType, args);
            if (_isSinglton)
                SingletonInstance = instance;            return instance;
        }
    }
}

Назначения и возможности у класса следующие:

  • есть возможность зарегистрировать класс для интерфейса Register<IInterface, FooClass>();
  • есть возможность зарегистрировать просто какой-то класс Register<FooClass>() (чтобы, например, использовать его в дальнейшем для создания объектов других классов, использующих его);
  • можно регистрировать одиночек, как с отложенным созданием RegisterSingleton<IInterface, FooClass>(), которые создадутся при первом вызове Resolve, так и с указанием объекта при помощи RegisterInstance<IInterface, FooClass>();
  • есть поддержка создания объектов с параметризированными конструкторами, если существуют уже зарегистрированные реализации.

Минусы у этой реализации очевидны – нет поддержки мультипоточности для отложенных одиночек RegisterSingleton. Совет тут простой, либо не используйте этот метод, и используйте вместо него RegisterInstance, либо допишите класс, добавьте семафор на создание объекта-одиночки. Еще можно добавить в класс доступ к объекту одиночке IoC, например, так:

private static readonly Lazy<IoC> _instance = new Lazy<IoC>(() => new IoC());
 public static IoC Instance
{    get { return _instance.Value; }
}
 private IoC()
{
}

В общем-то в момент, когда вам не будет хватать этой (либо вашей реализации IoC) можно просто взять и вставить использование внутри класса IoC одной из монстро-реализаций, вроде Unity. У меня все-равно, даже в проектах, которые используют Unity, он сам обернут в тот же IoC класс (а мало ли). Для Unity даже в этом есть плюс, не нужно подключать пространство имен Microsoft.Practice.Unity, чтобы использовать методы с параметризированными типами Register<T1, T2>() вместо Register(Type, Type), так как эти методы являются extensions methods, что сильно раздражает.

See Also