Реализуем сами простой IoC контейнер
- 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, что сильно раздражает.