Управление зависимостями между компонентами в приложении

Статья посвящена управлению зависимостями между компонентами в приложении, рассматриваются такие концепции как Dependency Injection, Investion of Control container, Service Locator. Рассматривается существующий подход к управлению зависимостями между компонентами в приложении, анализируются проблемы которые несет в себе этот подход. Далее, путем последовательных шагов делается попытка решить эти проблемы, в результате чего приходим к таким концепциям как Service Locator, Dependency Injection, Inversion of Control Container.

Основной упор в статье делается на вопросы "Зачем?", "Почему?", а не "Как?". Обычно этим вопросам не уделяется должного внимания, и упор делается на детали реализации, однако понимание глубинных причин использования тех или иных вещей критично для их правильного и своевременного употребления.

Статья ориентирована на разработчиков, которые:

  • не слышали об DI/IoC и хотят понять зачем нужно использовать эти новинки, если и "так все хорошо".
  • знают что это такое, но пока не использовали в реальных приложения, и хотели бы более глубже понять необходимость использования DI/IoC, и какие преимущества несет эта концепция.
  • те кто, используют DI/IoC, но хотели бы лучше понять какие проблемы решает DI/IoC и какие benefits несет.

Содержание:

  1. Вопрос управления зависимостями между компонентами в приложении
  2. Классический подход к управлению зависимостями внутри компонент приложения
  3. Использование Service Locator
  4. Использование Dependency Injection / Inversion of control container
  5. Service Locator vs DI/IoC Container
  6. Реализации IoC container'ов
  7. Полезные ресурсы

Вопрос управления зависимостями между компонентами в приложении

Объектно-ориентированный подход предписывает использовать объект как единицу повторного использования, а приложение строить в виде набора компонент, которые объединяются в систему, между которыми устанавливаются различного рода связи и зависимости, и как результат, эта структура позволяет реализовать определенные use case'ы. С эволюцией приложения количество компонент растет, а вместе с ними растет и количество зависимостей между ними, таким образом структура приложения усложняется, и ей становится сложно управлять.

Чтобы не быть голословным, в качестве примера рассмотрим гипотетическую cистему упаковки заказов для дальнейшей отправки их конечному пользователю интернет-магазина, который специализируется на электронной технике. Component Structure Описание некоторых компонент:
  • ShippingMethodCapabilities - позволяет определить может ли упакованный заказ быть отправлен конкретной службой доставки, в качестве критериев могут выступать размер, вес, содержимое упаковки, тип тары.
  • IPostalService - адаптер к сервису служб доставки (USPS, FedEx, UPS), знает как разговаривать с этими сервисами, отправляет им запросы на получение различной информации и получает от них ответы.
  • IPackingAlgorithm - представляет алгоритм упаковки предметов заказа. Различные алгоритмы смоделированы в виде таких классов, как TightPackingAlgorithm, SafePackingAlgorithm, TaggedPackingAlgorithm, WeightLimitedPackingAlgorithm.
  • IBox - предоставляет разные тип упаковки (в нашем примере: Small, Medium, Large).
  • DeviceWeightCalculator - считает вес контейнера, включая его содержимое, либо отдельно вес каждого устройства.

Предположим, разработчик воплотил в жизнь Single Responsibility Principle и спроектировал компоненты таким образом, что каждый занимается своим делом и делает это хорошо, и не знает больше чем ему это надо. Но редкий компонент живет в изоляции, он использует другие компоненты (зависимости), те в свою очередь используют свои, и т.д. Выстраивается структура приложения, причем структура эта далеко не статична, она динамична, она изменчива в зависимости от сценария использования приложения. На то компоненты и являются еденицами повторного использования, чтоб их использовать в разных сценариях.

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

  1. Определение нужных реализаций компонент исходя из некой логики. В нашем примере, это может быть выбор нужной реализации IPackingAlgorithm в зависимости от настроек клиента либо специфики заказа.
  2. Управление временем жизни зависимостей, а именно создание, уничтожение зависимостей в нужный момент времени. Для этого может потребоваться где-то хранить созданные компоненты как для использования, так и для последующего уничтожения.

И тут разработчик сталкивается с вопросом: "Где в приложении разместить всю эту логику управления зависимостей?". И это важнее, чем вопрос: "Как реализовать эту логику?". Это и есть вопрос управления зависимостями. Далее будут рассматриваться возможные варианты решения этого вопроса.

Управление зависимостями внутри компонент приложения

Если особо не задумываться над поставленным вопросом, то на первый взгляд кажется, что управлять зависимостями должен тот компонент, которому собственно и нужны эти зависимости. Такой подхож кажется даже логичным. Получаем что-то в этом духе.

public class DevicePacker
{
    private IPackingAlgorithm _algorithm;
    private ShippingMethodCapabilities _shippingMethodCapabilities;
    private BoxTracker_itemTracker;

    public DevicePacker()
    {
        // creation of required dependencies,  dependency managing logic
        _algorithm = new WeightLimitedPackingAlgorithm();
        _shippingMethodCapabilities = new ShippingMethodCapabilities(
            new FedexPostalService());
        _itemTracker = new BoxTracker();
    }

    public Package PackOrder(Order order)
    {
        //component own useful logic
        throw new NotImplementedException();
    }
}

Анализ проблем этого подхода.

Проанализируем различные use case, в которых может использоваться компонент приложения, и как это повлияет на логику управления зависимостей внутри этого компонента.

  1. Определение типа нужной зависимости может зависеть не только от внутренней логики компонента, а и от внешних для компонента факторов.

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

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

    public class WeightLimitedPackingAlgorithm : IPackingAlgorithm
    {
        private ItemInfoProvider _infoProvider;
        public WeightLimitedPackingAlgorithm(PackagingRuleProvider rules, Client client)
        {
            // this class does not use PackagingRuleProvider and Client at all
            // only pass them to the deeper layer
            _boxProvider = new BoxProvider(rules, client);
        }
    }
    
    public class BoxProvider
    {
        private IList<IBox> _itemPackers;
    
        public ItemInfoProvider(
            PackagingRuleProvider packagingRuleProvider,
            Client client)
        {
            //composite dependency creation logic which depends on outer context data
            if(client.IsVIPClient && packagingRuleProvider.DoesClientSupportBigBoxes)
            {
                _boxes.Add(new BixBox());
            }
            else if(packagingRuleProvider.GetBoxWeightLimit < 20.Ounces())
            {
                _boxes.Add(new MediumBox());
            }
            else
            {
                _boxes.Add(new SmallBox());
            }
        }
    }
    

  2. Определение типа зависимости зависит от сценария использования либо от режима работы.

    Компонент вынужден знать о конкретном use case, в рамках которого он выполняется, и учитывать это при разрешении зависимостей. В итоге теряется гибкость и возможность повторного использования компонента в других use case'ах. Также, при добавлении нового либо удалении старого use case'a приходится обновлять все компоненты, которые знают о них.

    К примеру, на production environment правила упаковки хранятся в базе данных, а на test environment - в XML файле, который расположен локально на файловой системе. В таком случае конкретная реализация IRuleRepository будет варьироваться.

        public DevicePacker(Client client)
        {
            // dependency resolve depends on deployment scenario
            if(Configuration.IsTestMode)
            {
                var packagingRuleProvider = new XmlRuleProvider();
            }else
            {
                var packagingRuleProvider = new SqlRuleProvider();
            }
        }
    

  3. Невозможность покрытия компонентов юнит тестами, которые бы верифицировали определенный аспект работы компонента в изоляции от его зависимостей.

    Если компонент сам создает необходимые ему зависимости, то заменить эти зависимости на "fakes" в целях тестирования становится невозможным. В итоге можно прийти к такому workaround, то есть делать инъекцию зависимостей через read-write свойства.

    [TestFixture]
    public class TestPackagingCreator
    {
        [Test]
        public void Should_pack_order_with_two_discs_in_single_mailer_if_rules_are_configured_as_not_vip()
        {
            var devicePacker= MockRepository.GenerateMock<DevicePacker>();
            var packagingRules = MockRepository.GenerateMock<PackagingRuleProvider>();
    
            // how to inject fake PackagingRuleProvider into DevicePacker instance???
            // let's expose dependency as read-write property and inject through it
            devicePacker.Stub(pc => pc.Rules).Return(packagingRules);
        }
    }
    

  4. Если компонент управляет временем жизни своих зависимостей, то он должен в какой-то момент времени уничтожать экземпляры созданных им зависимостей.

    Логика компонента снова же усложняется и помимо "полезной" логики, нужно "keep in mind" корректно завершить жизненный цикл созданный им объектов.

        public Package PackOrder(Order order)
        { 
           // create WeightCalculator instance with lifetime scope per order packing      
           // and destroy it after order is packed
           using(var DeviceWeightCalculator = new DeviceWeightCalculator())
           {
                var package = _algorithm.PackOrder(order);
                var weight = weightCalculator.CalculateWeight(package);
                if(_shippingMethodCapabilities.DoesExceeds(weight))
                {
                    // do something, for example, divide order in different packages
                }
                return package;
           }
        }
    

  5. Время жизни зависимостей не может быть больше, чем время жизни самого компонента.

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

  6. При наличии shared dependency, то есть зависимости, которая используется разными компонентами в разное время, непонятно кто из компонент должен управлять этой зависимостью.

    Что можно сделать в этом случае? Как сделать так, чтобы компоненты имели доступ к shared dependency?

    Да-да, реализовать зависимость в виде Singleton. Но тут стоит вспомнить притчу: "Если у вас есть одна проблема и вы пытаетесь решить её с помощью Singleton, то у вас есть две проблемы". Я бы сказал, даже не две, а больше. Вот некоторые из них:

    • Невозможность подмены зависимостей в тестах.
    • Что делать если время жизни shared dependency ограничего неким scope, отличным от per-application lifetime scope, к примеру, per-client processing scope или per-order processing scope.
    • Что делать, если доступ к зависимости происходит в мультипоточном сценарии? Надо еще наворачивать синхронизационные конструкции?

    Другой вариант состоит в том, чтобы перенести управление зависимостью на уровень компонента, который является parent'ом для тех компонент, которым нужна зависимость, и прокидывать зависимость, созданную в parent'е, тому компоненту, где она используется. В сложном приложении с несколькими layer of indirection это означает, что помимо засорения parent компонента знанием о зависимости, которая ему совершенно не нужна, мы еще засоряем и все промежуточные слои приложения от parent'а и до целевого компонента ненужными знаниями. Также это делает наше приложение монолитным, и внесение изменений, которые затрагивают зависимость, либо parent component, либо политику lifetime зависимости скорее всего затронет все слои, через которые зависимость проброшена, и цена таких изменений будет велика.

Недостатки управления зависимостями внутри компонент приложения

Итак, на основе проведенного анализа, можно составить список проблем. Если копнуть глубже, думаю, список проблем будет больше.
  1. Не соблюдается Single Responsibility Principle; компоненту необходимо помимо собственной "полезной" логики заниматься управлением зависимостями, и это получается у него плохо. Нарушается распределения знаний в системе. Различные части приложения засоряются знаниями, которые им на самом деле не нужны.
  2. Увеличивается coupling (cвязанность) компонент внутри приложения.
  3. Страдает re-usability компонента; компонент зависит от сценария использования, а сценарий использования зависит от компонента. Здесь можно провести параллель с нарушением Dependency Inversion Principle.
  4. Дублирование логики; многие компоненты создают зависимости на основании одних и тех же факторов и логики. Если факторы меняются, то надо обновлять все компоненты.
  5. Уменьшается maintainability приложения; Увеличивается цена и сложность внесения изменений.
  6. Невозможность изолированного тестирования компонент; а это может повлечь невозможность применения test-first development, либо test-driven development, либо behavior-driven development подхода, а это значит, что список проблем, с которыми нам придется столкнуться, увеличивается.
  7. Усложнение приложения; его структуры, потоков данных.
  8. Уменьшается уровень readability кода.

Здесь возникает вопрос: "А не пытаемся ли мы заставить наши компоненты заниматься тем, чем они не должны заниматься?".

Возможно, отдельно взятые компоненты приложения - не лучшее место для размещения логики управления зависимостями. А почему бы нам не возложить эту обязанность на абсолютно другой компонент, не имеющий отношения к приложению, некий внешний компонент "X"? Тогда компоненты приложения вместо того, чтобы самим создавать зависимости, запрашивали бы компонент "X" предоставить им нужную зависимость.

Service Locator

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

    public DevicePacker(ServiceLocator serviceLocator)
    {
        // dependency resolving depends on deployment scenario
        _algorithm = serviceLocator.GetService<IPackingAlgorithm>();
        _shippingMethodCapabilities = serviceLocator.GetService<ShippingMethodCapabilities>();
        _boxTracker= serviceLocator.GetService<BoxTracker>();
    }

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

Обычно проблема доступа к Service Locator из компонент приложения решается до боли известным способом: "Singleton". Да, чтобы компоненты приложения без лишней боли имели доступ к экземпляру Service Locator, этот самый Service Locator моделируется в виде Singleton.

public DevicePacker()
{
    // service locator is a singleton
    _algorithm = ServiceLocator.GetService<IPackingAlgorithm>();
    _shippingMethodCapabilities = ServiceLocator.GetService<ShippingMethodCapabilities>();
    _boxTracker = ServiceLocator.GetService<BoxTracker>();
}

Этот подход ничем не отличается от описанного выше, с тем лишь различием, что вместо моделирования многих компонент приложения как Singleton для доступа к ним, мы делаем это только с Service Locator'ом. И нам кажется, что на это можно закрыть глаза. Однако это вовсе не решает проблем, которые привносит использование Singleton, мы лишь уменьшаем масштаб бедствия и локализуем его.

И хотя Service Locator и решает частично некоторые проблемы из описанных выше, и его внедрение в legacy application позволяет уменьшить уровень связанности, но причина остается в прежнем виде:
Компоненты приложения инициируют процесс разрешения зависимостей (dependency injection), и не важно сами они это делают, или этим занимается Service Locator.

Dependency Injection / Inversion of Control Container

Идея состоит в том, чтобы не компоненты приложения просили зависимости у компонента "X" (контейнера компонент), а чтобы контейнер сам делал инъекцию зависимостей в те компоненты, которым они нужны. Тем самым мы инвертируем механизм инъекции зависимостей, и реализуем Hollywood principle, "Don't call us, we'll call you".

Отсюда и название Inversion of Control Container. Это название может быть сбивающим с толку, здесь важно понимать, что принцип Inversion of Control не является отличительной чертой контейнеров компонент, а является неотъемлимой чертой любого framework'a, и состоит в том, что application code не вызывает framework code, вместо этого framework code вызывает application code. Скорее нужно спросить: "К какому аспекту работы приложения применяется принцип inversion of control". Ответ: "Dependency injection". В итоге уточняем первоначальное название "IoC container" следующим "DI/IoC Container".

Основное преимущество использования DI/IoC состоит в том, что компоненты приложения освобождены от необходимости знать о контейнере, как это было в случае Service Locator, и не надо больше городить огород с Service Locator as a Singleton, и это позволяет нам решить проблемы описанные выше.

Dependency Injection Options

Если IoC Container занимается инъекцией зависимостей, то дизайн компонента должен позволять передать ему зависимость. Различают несколько вариантов передачи зависимостей в компонент.

  1. Сonstructor injection
        public DevicePacker(
            IPackingAlgorithm algorithm,
            ShippingMethodCapabilities shipingCapabilities,
            BoxTracker boxTracker)
        {
            _algorithm = _algorithm;
            _shippingMethodCapabilities = shipingCapabilities;
            _boxTracker = _boxTracker;
        }
    

    Все необходимые зависимости явно передаются компоненту через конструктор. Это наиболее предпочтительный подход. Так как зависимости передаются через конструктор, то невозможно создать компонент, если контейнер не располагает необходимыми зависимостями. Это лучше, чем создать компонент без нужной зависимости, и обнаружить эту проблему где-то намного позже. К тому же, код компонента становится более читабельным и не требующим разъяснений (self-explanatory), глядя лишь на конструктор можно понять какие зависимые компоненты нужны данному компоненту для работы.

  2. Property Injection
  3.     public DevicePacker(
            IPackingAlgorithm algorithm,
            ShippingMethodCapabilities shipingCapabilities)
        {
            _algorithm = _algorithm;
            _shippingMethodCapabilities = shipingCapabilities;
        }
    
        public BoxTracker BoxTracker { get; set; }
    

    Зависимости передаются через свойства. Менее предпочительный вариант, обычно через свойства передаются опциональные зависимости, без которых компонент может продолжать корректно работать. К тому же мы нарушаем инкапсуляцию.

  4. Method injection
        public DevicePacker(ShippingMethodCapabilities shipingCapabilities)
        {
            _shippingMethodCapabilities = shipingCapabilities;
        }
    
        public BoxTracker BoxTracker { get; set; }
    
        public void SetupAlgorithm(IPackingAlgorithm algorithm)
        {
            throw new NotImplementedException();
        }
    

    Довольно экзотический вариант. Не встречал в практическом использовании, возможно его имеет смысл использовать, если есть legacy сomponent, который принимает зависимости через метод, и мы хотим прикрутить использование DI/IoC и не можем поменять method injection на constructor injection либо property injection.

Component lifecycle and lifestyle

В управление зависимостями входит не только dependency resolve и dependency injection, но также и управление жизненным циклом компонент. Раз логика управления зависимостями вынесена в контейнер, то управление временем жизни компонент также должно входить в его обязанности.

Стандартный lifecycle компонента с точки зрения контейнера тривиален:

  1. Создание компонента.
  2. Его использование.
  3. Уничтожение компонента.

Необходимость осуществления контейнером этих шагов для конкретного компонента в конкретный момент времени регулируется политикой lifestyle, который указывается при регитсрации компонента в контейнере. Разные реализации контейнеров определяют разные наборы возможных lifestyle, однако можно выделить наиболее распространенные типы lifestyle'ов.

  • Transient, контейнер создает новый компонент каждый раз, когда осуществляется dependency injection. Контейнер не хранит ссылку на компонент, обязанность уничтожения компонента переносится на компонент приложения.
  • Singleton, контейнер создает один экземпляр компонента, время жизни компонента совпадает со временем жизни контейнера. Удовлетворяет все зависимости этим одним и тем же экземпляром.
  • Per Thread, создается один экземпляр компонента для каждого потока, время жизни компонента совпадает со временем жизни потока, то есть контейнер уничтожает компонент при завершении потока, в котором этот компонент был создан.
  • Per lifetime scope, приложение само определяет границы жизненного цикла, к примеру per-application, per web request, per client processing, per order processing. Контейнер хранит компонент и уничтожает его при выходе приложения из определенного lifetime scope.

Lifestyle компонента определяется при регистрации компонента в контейнера. Чтобы не углубляться в детали реализации, в этой статье возможные варианты регистрации компонент в контейнере рассмотрены не будут. Если есть интерес, то можно ознакомиться на примере регистрации компонент в Autofac'e.

Service Locator vs DI/IoC Container

Нередко Service Locator используется совместно с DI/IoC container'ом. Можно выделить два варианта такого совместного использования.

  1. Service Locator внутренне реализован на основании DI/IoC container'a и запрашивает зависимости у контейнера.

    Это не такой уж и экзотический case. Допустим, имеется существующее приложение, мы захотели прикрутить DI/IoC контейнер. Просто так это сделать у нас не получится, для этого скорее всего потребуется сделать редизайн компонент, чтобы они могли принять зависимость либо в форме Constructor Injection, либо Property Injection, и т.п. Также, скорее всего, сделать этот редизайн так, чтобы мы всецело переключиться на использование DI/IoC container у нас не получится. Вот тут на помощь приходит Service Locator. Однако у нас уже есть контейнер компонент, не хотелось бы в Service Locator'е дублировать логику контейнера, тогда делаем так, чтобы Service Locator запрашивал зависимости у контейнера, и возвращал их компонентам приложения.

    public class ServiceLocator
    {
        private IWindsorContainer _container;
    
        public ServiceLocator()
        {
            _container = new WindsorContainer();
            SetupContainer(_container);
        }
    
        public T GetService<T>()
        {
            return _container.Resolve<T>();
        }
    }
    

  2. DI/IoС контейнер injects Service Locator тому компоненту, где этот Service Locator нужен.

    Внимание, в предущем предожении английских слов больше чем русских :). Иногда сложно выразить технические вещи на русском :(.

    Это скорее логическое продолжение первого use case'а, когда у нас есть Service Locator и DI/IoC контейнер. Service Locator может быть реализован на базе контейнера, хотя в этом случае это необязательно. Здесь мы снова сталкиваемся с проблемой: "Как компонент приложения получит ссылку на Service Locator?". Только в этот раз вместо того, чтобы моделировать Service Locator в виде Singleton, мы регистрируем Service Locator в контейнере, и контейнер делает инъекцию Service Locator'a в нужное место.

    public interface IServiceLocator
    {
        T GetService<T>();
    }
    
    // service locator is based on DI/IoC container
    public class CastleWindsorServiceLocator : IServiceLocator
    {
        private IWindsorContainer _container;
    
        public CastleWindsorServiceLocator()
        {
            _container = new WindsorContainer();
        }
    
        public T GetService<T>()
        {
            throw new NotImplementedException();
        }
    }
    
    public class DevicePacker
    {
        private IPackingAlgorithm _algorithm;
        private ShippingMethodCapabilities _shippingMethodCapabilities;
        private BoxTracker _boxTracker;
    
        // inject service locator into the component
        public DevicePacker(IServiceLocator serviceLocator)
        {
            _algorithm = serviceLocator.GetService<IPackingAlgorithm>();
            _shippingMethodCapabilities = serviceLocator.GetService<ShippingMethodCapabilities>();
            _boxTracker = serviceLocator.GetService<BoxTracker>();
        }
    }
    

Сommon Service Locator

Ранее мы говорили, что при использовании Service Locator, компоненты приложения вынуждены знать о Service Locator'е в виде статической ссылки. Если Service Locator реализован на основании DI/IoC контейнера, то неплохо бы скрыть от компонента приложения внутреннюю реализацию Service Locator'а. Также нам может потребоваться заменить текущую реализацию контейнера на другую (Castle.Windsor на Autofac), и в этом случае мы не должны менять клиентов Service Locator'a.

Cмотреть сюда: Common Service Locator.

Реализации IoC Container'ов

Существует довольно много реализаций IoC контейнеров от разных vendor'ов. Список тех, что я знаю либо слышал, ниже.

Лично я предпочитаю использовать Autofac, от других контейнеров его выгодно отличает наличие развитой системы типов отношений между компонентом и его зависимостью. Помимо стандартного отношения использования имеются также:

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

Это дает дополнительную гибкость, и позволяет компонентам нe ссылаться на контейнер в виде прямой ссылки либо в виде Service Locator'а, а выражать требования к использованию зависимостей в виде различных типов отношений. Более детально здесь либо еще подробнее здесь. Но это скорее тема для отдельного поста.

Полезные ресурсы

В завершение статьи привожу список ссылок из своих bookmarks, которые могут быть полезны.

2 comments:

cerber said...

Хороший пост, охватил много моментов. Возможно, стоило упомянуть о "interceptors", часто они используются в связке. Хорошо, что есть граница между понятием "Dependency Injection" и "Inversion of Control", ибо их путают.

Павел Мартынов said...

Наверно лучший русскоязычный пост про DI/IoC, который я читал. Все очень просто описано, с примерами. Главное - всё идет от решения конкретной проблемы.
С нетерпением жду новых постов!

Post a Comment