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

Думаю, что даже уже начинающий разработчик должен быть знаком с понятием 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, что сильно раздражает.

Комментарии (70)

Андрей ( ) #
gravatar
До этого слышал про IoC немного, но лично никогда не сталкивался. И эта статья очень помогла понять что это такое и как работает. Спасибо!

Один только момент я не понял. Как это используется в моем коде %) ?
EvK ( ) #
gravatar
Я вот совершенно не понимаю - вроде модемы ушли в прошлое, а вы все еще экономите 300кб:) Не забывайте, что для запуска вашего приложения у пользователя должны быть 100мб фреймворка, так какой же смысл экономить 300кб? Если размер действительно критичен - пишите на С++. К тому же использование IoC уменьшает производительность приложения, но никто ведь об этом не переживает - поскольку архитектура становится гораздо прозрачнее. Так же и не стоит переживать из за лишних килобайт.
osmirnov ( ) #
gravatar
Для меня есть только одно оправдание не использовать перечисленые популярные IoC. Это - скорость. Но имхо это не причина писать свой. Мне нравятся следующие альтернативы:

1) Funq - Screencast (с примером написания своего IoC);

2) Munq - Article (с тестами производительности);
Дмитрий ( ) #
gravatar
Добрый день

Совершенно не знаком с IoC

Почему нельзя использовать интерфейс для каждой библиотеки вместо этого класса?

И как будет выглядеть использование этого класса в моем коде?
Vasiliy Novikov ( ) #
gravatar
To EvK,

При программировании под Azure приходится экономить на всём чтоб сэкономить деньгу. Думаю экономия ресурсов также актуальна и для WP7, поправьте, если не прав
Denis Gladkikh ( ) #
gravatar
Андрей, все очень просто, тут просто нужно подсесть на такое кодирование. Вот пример. Мы знаем, что в будущем будем отправлять сообщения, но через какой gateway не ясно, пока делаем простой интерфейс для отправки sms сообщений:
public interface ISmsSender
{
    bool Send(string number, string text);
}


Пишем реализацию этого интерфейса, который просто пишет сообщение в лог, например:
public class FooSmsSender : ISmsSender
{
    bool ISmsSender.Send(string number, string text)
    {
        Debug.Write(string.Format("Sending sms '{0}' to number '{1}', text, number));
    }
}
Дальше регистриурем нашу реализацию, где-то в инициализации приложения (Global.asax, например, при ASP.NET; Application Startup при WPF/WinForms/etc):
IoC.Instance.Register<ISmsSender, FooSmsSender>()
Ну а дальше, если нужно будет где-то использовать, то делается очень просто:
ISmsSender sender = IoC.Instance.Resolve<ISmsSender>();
sender.Send(sms.Number, sms.Text);
Соответственно, когда уже выберем провайдера для отправки sms, то в приложение пропишем только в одном месте конкретную реализацию (это где используется метод Register<,>), нам не нужно будет искать все места, где мы использовали отправку sms сообщений.

Дальше, нам такой подход облегчает написание тестов, очень легко делаем Mockup объекты, вместо самих реализаций для тестирования, что действительно сделает вызов метода Send и т.п.
Ilya ( ) #
gravatar
Небольшое замечание, не забываем начинать разработку с объявления интерфейса для IoC
Denis Gladkikh ( ) #
gravatar
EvK, про модемы это вы зря, не совсем они еще в прошлое ушли, точнее близкая к ним скорость не совсем ушла, много людей используют до сих пор gprs, а у многих еще 128 килобит ADSL линия, провинция, что сказать. И тут даже дело не в этом, не только в этом. Вот пишите вы Silverlight приложение, хотите запустить его в массы, ну сделать что-то вроде онлайн сервиса. Пихаете в него библиотеки от Toolkit, ну просто захотелось использовать DataGrid, а ваше приложение уже разраслось на полмегабайта в себе ничего особого не реализуя. Добавили Prism+Unity еще 300, почти мегабайт, а толку от приложения пока 0. Итого пользователь ждет пускай даже 10 секунд для того, чтобы ничего особого не увидеть. В общем, так вот просто приложение добирает и 10 мегабайт, итого я жду уже минуту для загрузки сайта на 2х мегабитном канале, а в результате может оказаться, что я вообще не тот сайт хотел открыть (я так часто в флеш сайтами обламывался).

Для просмотра сайта/работы с приложением пользователю так же нужно еще иметь Windows/MacOS/Linux, которые весят пару гигабайт, но это не дает право увеличивать ваше приложение до непонятных размеров непонятно зачем.
Denis Gladkikh ( ) #
gravatar
osmirnov, честно не понимаю, как IoC может влиять на производительность. Не нужно писать так:
List<Foo> list = new List<Foo>();
for (int i = 0; i < 1000; i++)
   list.Add(IoC.Instance.Resolve<Foo>());
Такое нужно заменять на:
FooFactory factory = IoC.Instance.Resolve<FooFactory>();
List<Foo> list = new List<Foo>();
for (int i = 0; i < 1000; i++)
   list.Add(factory.CreateNew());
И никаких проблем не будет. Можете расписать поподробнее, где и как у вас возникают проблемы с производительностью?

P.S. За приведенные IoC фреймворки спасибо.
Denis Gladkikh ( ) #
gravatar
Дмитрий, посмотрите, пожалуйста, один из предыдущих моих ответов, мне кажется там, примерно, был тот же самый вопрос: http://outcoldman.com/ru/blog/show/276#comment_1312. Если нет, то я просто не совсем понял вопрос "Почему нельзя использовать интерфейс для каждой библиотеки вместо этого класса?", можете поподробнее?
Дмитрий ( ) #
gravatar
Все, после примеров я все понял. Большое спасибо.
EvK ( ) #
gravatar
To Vasiliy Novikov:

Но это не тот случай. Если мы используем IoC для десктопного приложения, то не будем распространять его через Azure. Если это веб-приложение, то IoC будет использоваться на сервере и не будет передаваться клиенту, так что его размер значения не имеет. Что касается WP7 - использование IoC уменьшает производительность приложения, и именно это и нужно иметь в виду в первую очередь, а это гораздо важнее размера исходника (даже для мобильных телефонов).
Denis Gladkikh ( ) #
gravatar
Ilya, в смысле для подмены IoC фрейморка в будущем? Я делаю не так, я скрываю фреймворк в IoC классе, то есть, если нужно или захочется использовать другой, то просто переписываю IoC класс. Имхо, иметь две реализации IoC для приложения вряд ли необходимо, вряд ли будет необходимо перекидывать с одного на другой фреймворк приложение. Другой вариант, если пишите фреймворки сами, типа Prism и оставляете пользователю возможность выбора IoC, просто говоря какой интерфейс вам нужно подпихнуть для работы вашей либы.
Denis Gladkikh ( ) #
gravatar
EvK, реально не понимаю про производительность приложения, где он может повлиять? Создание одного ViewModel для окна через IoC, при клике пользователя, на производительность никак не повлияет, даже создав 10 классов через IoC.
Sergey Popov ( ) #
gravatar
EvK

В статье, кстати, указано, когда это оправдано на 100% - это различные frameworks & libraries. В этом случае приложение может использовать любую реализацию IoC, и для подключения библиотеки (framework) нужен только небольшой proxy.
Андрей ( ) #
gravatar
Денис, спасибо за такой поднобный ответ. Я бы, наверное, в подобной ситуации написал что-то похожее на:
public static class SmsSenderCreator
{
   public static ISmsSender Create()
   {
      return new FooSmsSender();
   }
}
А сейчас понимаю, что вариант с IoC позволяет задать реализацию для интерфейса в рантайме, т.е. более гибкий. А если в приложении не нужна такая гибкость, то имеет смысл использовать IoC? Может я упускаю еще какие-то дополнительные бонусы, которые дает его использование?
Sergey Popov ( ) #
gravatar
Denis Gladkikh А можно я отвечу на последний вопрос?.. :)
Aleksey ( ) #
gravatar
Спасибо за статью, но очень мало возможностей у этой реализации, даже непонятно, как ее с пользой использовать =)

Да и стоит ли переживать из-за лишних килобайт, когда есть, например, реализация Autofac для Silverlight, весит всего 99 КБ.
Denis Gladkikh ( ) #
gravatar
Андрей, все бывает в первой, лучше на это закладываться. Ну и такая реализация позволяет гибче тестировать код, подменяя реализации.

Aleksey, для меня часто бывает, что больше и не нужно, если это не enterprise проект, который собирается по частям.
Nick Sergeev ( ) #
gravatar
Тут вот выше пример приводили:
public static class SmsSenderCreator
{
   public static ISmsSender Create()
   {
      return new FooSmsSender();
   }
}
и озвучили минус, что нельзя в рантайме подменить реализацию. Но если сделать следующее
public static class SmsSenderCreator
{
   public static ISmsSender Instance {get;set;}
}
то мы можем установить Instance в том же Global.asax, и при необходимости его подменить.

Дак вот вопрос, чем этот подход будет хуже чем IoC?

И второе, чем Вас так раздражают extension методы? :-)
Denis Gladkikh ( ) #
gravatar
Nick Sergeev, в принципе, ваш вариант тоже годится, в каком-то смысле SmsSenderCreator и есть в вашем последнем примере ограниченный IoC контейнер. Минус тут только в том, что когда интерфейсов с реализациями будет много, то на каждый писать отдельный Creator проблематично. К тому же у вас только есть поддержка Sigleton, да и без отложенного создания объекта (правда это все опять же можно дописать для приведенного Creator'а по мере необходимости). В каком-то смысле IoC контейнер позволяет нам избежать написания множества Creator классов (ну или простым языком Фабрик по созданию и хранению, определенных объектов).

С Extension методами есть одна неприятная проблема. Вот пихнули вы, например, вот так свой IoC контейнер:
public class IoC
{
   public static UnityContainer Container {get;set;}
}
Дальше, где-то используем этот контейнер:
class Foo
{
   public static void Main()
   {
      var obj = IoC.Container.Resolve<SomeFooType>();
   }
}
Скомпилируется такой код? Нет, нужно добавить еще ни кому не нужный using Microsoft.Practice.Unity. Лишняя работа. Очень плохая реализация, не понимаю о чем думали архитекторы этой либы. Еще пример, копипастишь код, и не понимаешь почему это после копипаста у тебя метод не определяется (в случае не знания реализации Unity, честно признаюсь что когда-то я тоже подсел на эту проблему, и потратил тупо минут 10, прежде чем понять в чем дело, проверял версии библиотек и т.п.). Extension методы хороши, когда их реализация может быть пригодна много где, как тот же Linq, его можно использовать для разных и разнообразных колекций. А писать методы, которые можно использовать только с объектами одного и того же типа, как extension, имхо, очень глупо без реальной необходимости (вроде закрытости типа, в смысле находится в отдельной либе).
г-н Тараканофф ( ) #
gravatar
Офигеть! Спасибо. Если эта штука работает быстрее, чем Microsoft.Unity, то буду использовать её в своих проектах.
Евгений Шапиро ( ) #
gravatar
> Еще пример, копипастишь код, и не понимаешь почему это после копипаста у тебя метод не определяется

Денис, очень советую ReSharper. Достаточно сказать, что указанную ситуацию в рамках одного solution'а он разрулит. Для нормальных методов он еще может ссылки на сборку автоматом добавить.

Мне кажется в ходе всего разговора упущена существенная выгода рекурсивности инициализации объектов, которые достаются из контейнера.
OlegAxenow ( ) #
gravatar
Дэн, что касается семафоров - IMHO, ReaderWriterLockSlim лучше подходит для этой задачи.

На работе последний год тоже активно пользуюсь самописным IoC для простейших случаев - он еще проще твоего даже с учетом многопоточности, потому что и случаи еще проще :)

P.S. Кстати, еще один вариант дальнейшего развития - ускорение за счет использования заранее подготовленных делегатов - это все же быстрее Activator'а.

P.P.S. Ни в коем случае не предлагаю меряться IoC'ами :)
Denis Gladkikh ( ) #
gravatar
Евгений Шапиро, да знаем мы про R#, активно пользуемся, но и он не всесилен, и в данной ситуации не поможет. Попробуйте сначала ;)

Я вот даже не поленился проверить:
Denis Gladkikh ( ) #
gravatar
OlegAxenow, спасибо за замечания. Согласен, ReaderWriterLockSlim в данном случае будет уместен. С другой стороны все, конечно же, зависит от использования. Чаще всего все регистрации происходят во время запуска приложения, в случае WPF/SL/WinForms проблем совсем нет, так как конфигурацию производим перед запуском главной формы. В случае веб-приложения - на Application_Startup или как там event называется у Global.asax, производим конфигурацию IoC с локом кода (все равно дальше код выполнять нельзя, пока не загрузим все что нужно в конфигурацию). ИМХО, ситуации, когда нужно конфигурировать в рантайме реализации бывают конечно же, но крайне редки, у меня по крайней мере их не было совсем.

Олег, я вот все не понимаю, зачем существует необходимость разгонять IoC контейнер? Чтобы, в случае использования ASP.NET MVC, шустрее создавались контроллеры (знаю там как-то можно написать свою фабрику контроллеров, заместо дефолтной)? И то я не очень знаю лайфцикл контроллеров, они на каждый запрос создаются или нет? Вообщем, где от этого профит?
OlegAxenow ( ) #
gravatar
> ИМХО, ситуации, когда нужно конфигурировать в рантайме реализации бывают конечно же, но крайне редки

Полезно как для отложенной инициализации, так и для отслеживания изменения конфигурации без перезапуска приложения. К тому же, ReaderWriterLockSlim настолько просто использовать... На клиентской машине, естественно, все это не особо интересно.

> Олег, я вот все не понимаю, зачем существует необходимость разгонять IoC контейнер?

Навскидку - считывание данных в объектную модель параллельно работающими пользователями (без использования кэша объектов). В принципе - любой пример, когда создается либо большое количество, либо часто и многими потоками экземпляры более чем 16 (в .NET 2.0 размер "кэша" у Activator, для 4.0 не интересовался) разных типов.
Sergey Zwezdin ( ) #
gravatar
Было бы неплохо реализовать в твоем новоиспеченном контейнере интерфейсы IServiceContainer/IServiceProvider. Некоторые разработчики любят работать именно с ними. Тогда и упаковывать в дополнительный клас ничего не придется.
Denis Gladkikh ( ) #
gravatar
Навскидку - считывание данных в объектную модель параллельно работающими пользователями (без использования кэша объектов).
Олег, можешь объяснить поподробнее? Не понимаю зачем тебе в этом случае IoC.
osmirnov ( ) #
gravatar
Цикл, который вы привели безусловно ускоряет скорость работы, но разница в нагруженных веб-приложениях между приведенными контейнерами, вашей реализацией и популярными IoC всё равно заметна. На мой взгляд причина в том, что популярные IoC реализуют много чего, что не всегда нужно: иерархии контейнеров, инициализаторы, АОП, и т.п. Плюс реализация не всегда на уровне. Например, метод Activator.CreateInstance самый медленный метод создания объекта.
OlegAxenow ( ) #
gravatar
> Олег, можешь объяснить поподробнее? Не понимаю зачем тебе в этом случае IoC.

В одном из проектов использовали исключительно только для создания экземпляров определенных типов - возможностью менять типы для одного интерфейса не пользовались, но, при желании, можно было бы и это сделать.
Max Paulousky ( ) #
gravatar
много примеров реализации самописных контейнеров в WP7 проектах на codeplex.

Тот же Justin Angel в проекте http://neurons.codeplex.com/ использует свой контейнер. Правда, в проекте http://unitednations.codeplex.com/ уже перешёл на NInject с небольшой надстройкой
Mihail ( ) #
gravatar
Как раз пытаюсь разобраться с принципом Dependence Injection. По-моему реализация IoC в данной статье (если сделать класс статическим) - это чистой воды ServiceLocator.

У меня вопрос, не подсадим ли мы наше приложение на "клей" IoC таким вот макаром?:
private IProductService productService;
 
poublic HomeController(IProductService productService)
{
    this.productService = productService
}
 
public HomeController():this(IoC.Resolve<IProductService>)
{
}
Или наша реализация IoC будет выполнять активную инъекцию зависимости подобно Ninject и мы сможем оставить только один конструктор:
private IProductService productService;
 
poublic HomeController(IProductService productService)
{
    this.productService = productService
}
Denis Gladkikh ( ) #
gravatar
Mihail, можно оставить один конструктор, код должен выполниться, если тип IProductService зарегистрирован.
Mihail ( ) #
gravatar
Просто не могу понять саму технику.

Процесс регистрации и ресолва объектов это понятно. А вот как наш IoC отслеживает, что при инициализации некоего объекта ему в конструктор нужно передать реализацию некоего интерфейса не совсем понятно.

И еще вопрос, правильно ли я предполагаю, что второй вариант инъекции предпочтительней первого?
// 1-ый вариант:
private static Configuration Configuration
{
   get {
       if (configuration == null) {
           configuration = IoC.Resolve<INHibernateInitializer>().GetConfiguration();
       return configuration;
   }
}
// 2-ой вариант
private static Configuration GetConfiguration(IInitializer initializer)
{
   if (initializer == null) 
       throw new ArgumentNullException("initializer");
   return initializer.GetConfiguration();
}
Denis Gladkikh ( ) #
gravatar
Mihail, посмотрите на метод ResolveConstructorParameters, там берется первый конструктор, и для каждого параметра этого конструктора происходит попытка зарезолвить объект.

По поводу предпочтительного варианта инъекции не понял, там, вроде разные интерфейсы участвует в нем. Вообще, я считаю, что должен быть в приложении только одно статическое свойство - IoC, все остальное (остальные синглтоны) можно получить из него.
Mihail ( ) #
gravatar
Фух, спасибо, понял. Берется первый конструктор и по цепочке резолвятся все зависимости.

На счет интерфейсов в моих вариантах - чуть не доглядел, ну предположим в обоих случаях IInitializer, но не суть дело.

И еще хотел уточнить - статический IoC это есть ServiceLocator?
Nick Sergeev ( ) #
gravatar
Denis Gladkikh, вы писали:

Минус тут только в том, что когда интерфейсов с реализациями будет много, то на каждый писать отдельный Creator проблематично.

Развил тему в отдельный пост, DI против фабрики
Denis Gladkikh ( ) #
gravatar
Mihail, если во втором варианте у вас был бы конструктор, то Nick Sergeev более чем ответил на ваш вопрос (спасибо, ксатати, ему за этот пост, очень интересно).

По поводу ServiceLocator, я бы спросил у сообщества, так как я не могу утверждать на 100%, получается что ServiceLocator = Фабрике, описанной в посте Nick Sergeev.
Максим ( ) #
gravatar
Хотел бы спросить - а существует ли кастомная ("легкая") реализация EventAggregator from Prism/MEF? Просто не хочется к СЛ приложению лепить лишние ДЛЛки. Кстати ваша реализация IoC очень помогла с ними бороться :)

Спасибо
Максим ( ) #
gravatar
Ну вот так всегда - ищешь-ищешь, ничего не находишь, задаш вопрос и через 5 минут сам находишь ответ.

Вот что я только что нашел по моему вопросу: http://www.minddriven.de/index.php/technology/development/design-patterns/event-aggregator-implementation
Denis Gladkikh ( ) #
gravatar
Можно еще сделать и по-другому просто стащить 4 класса и пару интерфейсов из Prism библиотеки, вот сам Event Aggregator, ну и дальше по зависимостям еще немного классов вытяните.
mas ( ) #
gravatar
Извините за флейм, но...

Да IoC контейнер - это крутая игрушка - здорово. Но насколько часто вообще бывает реально необходимо в рантайме выбирать реализации интерфейсов или методов? В ваших проектах часто и повсеместно возникает такая насущная необходимость? Или просто статические классы - это скучновато и уже не так модно, как бывало в старые добрые времена?

Да, пример SmsSender показывает *как* это можно сделать, но оставляет открытым вопрос *зачем*! Почему это надо делать именно в рантайме, да ещё и для десятков (а при небольшом их числе не проще ли будет руками) различных классов?

Припоминаю всего около двух случаев (примерно за 3 года программирования на С++), когда обнаруживался реальный резон (то есть явная польза для логики и архитектуры системы) для чего-то подобного. В припоминаемых случаях вопрос касался одного-двух классов и решался вручную (при помощи указателя и семафора из winapi - да, это были многопоточные приложения) за 5 минут без затруднений, а также без глубоких мыслей об IoC, фреймворках и т.п.

Вся это история мне больше всего напомнила вот что: "Вы пытаетесь прострелить себе ногу, но осознаете, что для этого вам нужен 5 диск MSDN, Visual Studio .NET и … " (с)   и обязательно обобщённый IoC контейнер с поддержкой многопоточности, да, конечно же: ещё не забыть RPC на базе технологии SOAP и управление разграничением доступа на нескольких уровнях
Sergey Popov ( ) #
gravatar
mas

Раз 15 уже сказали - ЭТО НУЖНО ДЛЯ ПОДМЕНЫ РЕАЛИЗАЦИИ ПРИ ТЕСТАХ.
mas ( ) #
gravatar
Ок, при тестах. Но в рантайме-то зачем?
#ifdef DEBUG
#define SmsSender TestSmsSender
#endif
и т.п. Не подходит? Или проблема в том, что C# нет препроцессора и потому единственный выход - проводить тесты путём динамической рекомпиляции :)
Аноним ( ) #
gravatar
mas

IoC уменьшает связность между компонентами. Самый, пожалуй, распространненный случай - подмена реализации при тестах. Но кроме этого уменьшение связаности всегда положительно сказывается на архитектуре.

P.S. Вы не могли бы пояснить, что значит "проводить тесты путём динамической рекомпиляции"?
Denis Gladkikh ( ) #
gravatar
mas, можно и так сказать :) в c++, действительно, можно сделать как вы пишите, сделать два заголовочных файла описывающих такие подстановки, один для самого проекта, а второй для тестов.

Но будут проблемы. В одном тесте нужно использовать TestSmsSender, а во втором TestSmsSender2, а в третьем сам SmsSender. Конечно, для каждого теста можно использовать свои переопределения, тоже выход. Но все-таки с Mock и Stub фреймворками немного полегче будет писать тесты.

IoC, конечно же, хорош для enterprise разработки, где нет ничего постоянного, где нужно иметь тех же SmsSender около 5 на инсталяцию, так как сегодня может этот gateway отвалится, а завтра тот, и нужно шустро все настраивать. Это делается, например, при помощи xml настройки для Unity. Так же нужно уметь шустро, не пересобирая приложения, подменять иногда и другие компоненты системы.

И вот тут возникла другая ситуация. Мы, enterprise разработчики, просто привыкли к IoC, и не видим как это можно разрабатывать без него (а раньше можно было же). Потому, иногда, нам не нужен мощный Unity, но нужен простой IoC, который просто умеет регистрировать и резолвить типы, а если что-то выростет, то потом подставим и unity вместо него.
mas ( ) #
gravatar
Во-первых, Denis Gladkikh - спасибо за взвешенный и обстоятельный ответ. В целом вы подтвердили то, что я предполагал. Просто первоначально я выразил своё мнение в заведомо чрезмерно категоричной форме.

Да фреймворки для юнит-тестов - наверное, вещь полезная.

А я поступаю так - создаю небольшие раздельно компилируемые программы для основных групп юнит-тестов. Каждая такая программа подключает тестируемые (и по-минимуму прочие необходимые) классы, объявляет вручную свои моки и стабы, затем прогоняет серию тестовых случаев. Результаты (с некоторыми деталями) выдаются в stdout.

Вместе всё работает следующим образом: строим пакет тестов №1, запускаем его, результаты перенаправляем в файл, затем - пакет №2 и т.д. Сравниваем все полученные файлы результатов с соответствующими им файлами-образцами, смотрим diff-ы. Этот этап автоматизирован BAT-файлом :) Далее смотрим полученные diff-ы (если есть), анализируем изменения, фиксим баги, обновляем образцы (когда изменения обоснованы).

Естественно, сами тесты при этом пишутся так, чтобы выдаваемый текст менялся только если "случилось что-то серьёзное". И не с т.з. внутренней реализации тестируемого класса, а с т.з. его использования, т.е. неявной спецификации, "контаркта".

Мне так работать удобно. Первые юнит-тесты рождаются прямо из того отладочного кода, который пишется вместе с классом при его создании. Далее добавляются и уточняются. В итоге выходит несложно и весьма толково - можно наглядно отслеживать и контролировать значимые изменения в логике системы. С использованием простейших средств.

«Вы не могли бы пояснить, что значит "проводить тесты путём динамической рекомпиляции"?»

Это, конечно же шутка...   хотя какие тут шутки, всё серьёзно:   http://habrahabr.ru/blogs/net/67431 - уже есть положительные отзывы, похоже и к этому тоже скоро привыкнут и по-другому писать код не захотят :)
Sergey Popov ( ) #
gravatar
mas

Вы, конечно, извините, но ваш подход к тестам напоминает строительство дома из глины и соломы в эпоху небоскребов.. Посмотрите специализированные frameworks, не пожалеете.

Это, конечно же шутка... хотя какие тут шутки, всё серьёзно: http://habrahabr.ru/blogs/net/67431 - уже есть положительные отзывы, похоже и к этому тоже скоро привыкнут и по-другому писать код не захотят :)

А что, собственно, вам не нравится? Без наездов, мне действительно интересно.
mas ( ) #
gravatar
«А что, собственно, вам не нравится?» Да разве мне что-то не нравится? Скорее шучу просто. Есть такая тенденция - современные программисты для того, чтобы написать 100 строчек совершенно тривиального по своим функциям кода (ну например, взять строчку из БД, закодировать в XML и отправить в смс-гейт), считают своим долгом подключить десятки мегабайт библиотек, фреймворков и непременно использовать несколько самых навороченных модных фишек.

Моё личное мнение - большинство из этих фишек через пару лет забывается, потому, что им на смену приходят новые, ничем не лучше, просто новее :) Прогресса нет, есть смена моды. Вот недавно все разговоры кругом были на тему ООП, C++, GoF Design Patterns, stl... Здесь и сейчас имеем C#, SOLID, IoC, Unity... А всё об одном, о вечном :)

Вот вы говорите: «IoC уменьшает связность между компонентами» Да я вам приведу десятки ну оочень похожих "модных слов", которые пропогандировались как решение той же самой задачи.. актуальных поныне, почти забытых, забытых, чисто исторических. А только воз..? сдвинулся за последние годы? Как вы считаете?

Но прагматика вопроса состоит в том, что если эти методы вам помогают работать, экономить время и больше зарабатывать - значит они существуют не зря. Надеюсь это действительно так.
Sergey Popov ( ) #
gravatar
max

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

Приведите, с интересом послушаю :)

Но прагматика вопроса состоит в том, что если эти методы вам помогают работать, экономить время и больше зарабатывать - значит они существуют не зря. Надеюсь это действительно так.

Именно. Если C# берет на себя управление памятью в 99% случаев – я буду им пользоваться. Если LINQ позволяет 3 строки записать в одну – я тоже буду им пользоваться. Ну и т.п.
mas ( ) #
gravatar
Про фреймворки для тестирования, согласен - надо посмотреть. Когда в последний раз смотрел, увидел много модного и почти ничего полезного. Пробежав по примерам и документации, решил, что не стоит тратить время на установку и освоение. Может быть сейчас уже что-то изменилось.

Да, кстати, Sergey, а вы уверены, что мой глиняный дом не окажется выше вашего небоскрёба? Я бы не был столь категоричен. Суть не в модных фишках, а во владении фундаментальными знаниями. И результаты скорее всего зависят просто от того, как голова работает, и растут ли руки откуда надо :) Но я ничего о вас не знаю, поэтому не хочу ничем меряться - хорошо, согласен, просто боюсь проиграть :))
Sergey Popov ( ) #
gravatar
mas

Что касается меня, то постоянно нахожу для себя новые рабочие паттерны, идеи и т.п. В production идет далеко не все, но IMHO если не пробовать ничего нового, можно случано и фундаментальные изменения пропустить..
Sergey Popov ( ) #
gravatar
mas

Да, кстати, Sergey, а вы уверены, что мой глиняный дом не окажется выше вашего небоскрёба? Я бы не был столь категоричен. Суть не в модных фишках, а во владении фундаментальными знаниями. И результаты скорее всего зависят просто от того, как голова работает, и растут ли руки откуда надо :) Но я ничего о вас не знаю, поэтому не хочу ничем меряться - хорошо, согласен, просто боюсь проиграть :))

Да, я уверен. Т.к. когда к знаниям прикладываются фишки, к результату можно прийти значительно быстрее :)
Зашедший ( ) #
gravatar
А можно я нелегально использую ваш контейнер в своём приложении, а?
Denis Gladkikh ( ) #
gravatar
Зашедший, весь код в блоге можно использовать как угодно ;)
Зашедший ( ) #
gravatar
Ура!!! У меня тогда вопросик есть, если можно. Предположим что в графе зависимостей объектов есть объекты которые зависят от одного объекта, при этом такая ситуация повторяется несколько раз.Как тут быть.

Ioc-контейнер если использовать его для создания singleton(а) не поможет, так как для каждой такой ситуации он вернёт один и тот же объект. Что тут можно сделать?
Зашедший ( ) #
gravatar
Молчание... Меня приняли за троля. Ну ладно, вопросов больше не имею.
Denis Gladkikh ( ) #
gravatar
Зашедший, я не очень понял в чем вопрос. Есть проблема зависимостей - решайте, IoC в этом бывает полезен.

А по поводу singleton совсем не понял. IoC конейнеры могут создавать инстансы в единственном экземпляре.
Help ( ) #
gravatar
Решил использовать эту штуку, и вот: она отказывается создавать объекты если конструктор не public. А public их делать нельзя, потому что это домен и возвращать их должны корневые агрегаты. Что делать?
Denis Gladkikh ( ) #
gravatar
Help, немного изменил создание объектов (про Silverlight не проверял - будет ли там работать, в принципе, должно):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
 
namespace SomeNamespace
{
    public class IoC
    {
        private BindingFlags _bindingFlagsAllInstanceCtors = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
        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)
        {
            Register(typeof(TType), typeof(TConcrete), isSingleton, instance);
        }
 
        private void Register(Type type, Type concreteType, bool isSingleton, object concreteInstance)
        {
            if (_registeredObjects.ContainsKey(type))
                _registeredObjects.Remove(type);
            var registeredObject = new RegisteredObject(concreteType, isSingleton, concreteInstance);
            _registeredObjects.Add(type, registeredObject);
        }
 
        private object ResolveObject(Type type)
        {
            if (type == GetType())
                return this;
 
            if (!_registeredObjects.ContainsKey(type))
            {
                if (!type.IsInterface && !type.IsAbstract)
                    Register(type, type, false, null);
                else
                    throw new ArgumentOutOfRangeException(string.Format("The type {0} has not been registered", type.Name));
            }
            return GetInstance(_registeredObjects[type]);
        }
 
        private object GetInstance(RegisteredObject registeredObject)
        {
            object instance = registeredObject.Instance;
            if (instance == null)
            {
                var constructorInfo = registeredObject.ConcreteType.GetConstructors(_bindingFlagsAllInstanceCtors).First();
                var parameters = constructorInfo.GetParameters().Select(parameter => ResolveObject(parameter.ParameterType));
                instance = registeredObject.CreateInstance(constructorInfo, parameters.ToArray());
            }
            return instance;
        }
 
        private class RegisteredObject
        {
            private readonly bool _isSinglton;
 
            public RegisteredObject(Type concreteType, bool isSingleton, object instance)
            {
                _isSinglton = isSingleton;
                ConcreteType = concreteType;
                Instance = instance;
            }
 
            public Type ConcreteType { get; private set; }
 
            public object Instance { get; private set; }
 
            public object CreateInstance(ConstructorInfo ctor, params object[] args)
            {
                NewExpression newExp = Expression.New(ctor, args.Select(x => Expression.Constant(x)));       
                LambdaExpression lambda = Expression.Lambda(typeof(Func<object>), newExp);
                Func<object> compiled = (Func<object>)lambda.Compile();
 
                var instance = compiled();
                if (_isSinglton)
                    Instance = instance;
                return instance;
            }
        }
    }
}
Help ( ) #
gravatar
Денис, спасибо! Осталось только разобраться как это теперь работает...
Алексей ( ) #
gravatar
Знаете что ему не хватает до полноценного ioc-контейнера? Не хватает возможности регистрации типов из xml-файла и загрузки типов из сборок через рефлексию. Когда в проекте много сборок и контейнер пробрасывается в конструкторы, в случае вашей реализации сразу образуются циклические ссылки между сборками.

Кстати возможность регистрации типов через рефлексию - это очень важное свойство ioc-контейнера, без нее он всего лишь более удобная реализация абстрактной фабрики.
Denis Gladkikh ( ) #
gravatar
Алексей, тогда тут кода будет не меньше, чем в каком-нибудь ms unity container, и смысла в своем простом уже не будет.
Алекс ( ) #
gravatar
Спасибо большое за статью, очень полезная. Не могли бы пояснить момент

private static readonly Lazy _instance = new Lazy(() => new IoC());

public static IoC Instance

{ get { return _instance.Value; }

} Почему мы просто не можем тут создать экземпляр сразу get {return new IoC()} я пробовал не получилось конечно, но не могли объяснить как это работает?

Огромное спасибо.
Denis Gladkikh ( ) #
gravatar
Алекс, если вы напишите get {return new IoC();} , то каждый раз, когда вы будете обращаться к статическому свойству IoC.Instance для вас будет создаваться новый экземпляр. Использования Lazy - это чисто синтаксический сахар, который позволяет откладывать создание объекта до первого запроса, а не когда будет проинициализирован тип IoC.
Алекс ( ) #
gravatar
Спасибо да понятно. Глупый вопрос был сорри. Еще не могли бы объяснить чуть более про методы

RegisterSingleton

RegisterInstance

пока что я понял из примеров для чего Resolve ну и Register, а что делают последние не совсем понятно

Не могли бы немного пояснить.

Спасибо!
Denis Gladkikh ( ) #
gravatar
Алекс, RegisterSingleton и RegisterInstance позволяют зарегистрировать в IoC типы таким образом, что при каждом Resolve контейнер будет возвращать один и тот же объект, то есть Singleton, а не создавать каждый раз новый. Разница между RegisterSingleton и RegisterInstance только в том кто и когда создаст этот самый объект, в случае RegisterSingleton - IoC создаст его за вас, а в случае RegisterInstance - вы указываете какой объект стоит использовать.
Алекс ( ) #
gravatar
Денис огромное спасибо. Трудновато дается программирование, очень радуюсь когда начинаю понимать суть. Очень хорошая статья.

Большое спасибо Вам!!!
Denis Gladkikh ( ) #
gravatar
Эта статья получила продолжение. Как оказалось, на самом деле реализация с Expressions проигрывала изначальной реализации при помощи Activator, подробнее тут Оптимизируем свой собственный Inversion of Control Framework
Добавить комментарий
Если вы хотите получать уведомления о новых комментариях к данному топику, укажите, пожалуйста, email и отметьте соответствующий пункт в форме. Если вы хотите добавить код в тексте комментария, то заключите его внутри тега [code]...[/code], более того можно уточнить язык, на котором написан данный код при помощи [code cs]...[/code], где вместо cs могут быть cs, html, xml, java, js, php, sql, cpp, css.

 

busy