Заметки об IoC и DI

 
 

IoC - паттерн инверсии управления (Inversion of Control), почитать можно тут DI - паттерн внедрения зависимостей (Dependency Injection), почитать можно тут

В последнее время все чаще участвую в разного рода дискуссиях касательно архитектуры приложений, причем не важно каких - web, десктопных или мобильных. Во всех случаях всплывают примерно одни и те же проблемы и примерно одни и те же грабли. Дабы не “мозолить” себе язык и не объяснять свою точку зрения постоянно, решил написать эту статью. Тут рассмотрим некоторые основные подходы и паттерны и затем возможные их применения при разработке приложений.

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

protocol Inbox {
    func allLetters() -> [Mail]
}

class EmailInbox : Inbox { ... }

class MailService
{
    var inbox: Inbox = EmailInbox("null@example.com")

    func mail(with recipient: String) -> [Mail]
    {
        let letters = inbox.allLetters()
        return letters.filter { $0.recipient == recipient }
    }
}

Примерчик простой, однако даже в нем уже есть проблема - класс MailService крепко-накрепко связан с классом EmailInbox, несмотря даже на то, что мы честно выделили протокол Inbox. Теперь (например, в другом проекте) потребовалось использовать MailService, но при этом Inbox там реализован по другому (например через файлы). Это приведет к тому, что придется код переписывать, не получится просто взять и использовать.

Ситуацию можно немного ухудшить, написав примерно так.

class MailService
{
    func mail(with recipient: String) -> [Mail]
    {
        return EmailInbox.shared.allLetters().filter { $0.recipient == recipient }
    }
}

Теперь внешнему наблюдателю сразу даже будет непонятно, что внутри класса есть зависимость. Да при детальном анализе кода будет ясно, но если класс не из 10 строк, а из 1000? По этой же причине опасны singleton’ы.

Для обеспечения переносимости кода, для возможности организовать хорошее тестирование (например заmock’ать какие-нибудь зависимости) и т.д. с этим надо что-то делать. И вот тут-то и появляются разного рода умные слова, такие как Dependency Injection, Inversion of Control и иногда Service Locator. Как правило, считается что они значат примерно одно и то же, хотя если разбираться детальнее разница между ними есть и большая.

Самый первый и самый с одной стороны простой термин - Dependency Injection или по-русски “внедрение зависимостей”. Вообще, это комплекс мероприятий в результате которого зависимости оказываются в вашем объекте. Даже наш простой примерчик в общем делает dependency injection в самом простом его виде - зависимость просто создается. Внедрять зависимости можно спрашивая их у кого-то (см. Service Locator) или кто-то их нам установит автоматически (см. Inversion of Control).

let mailService = MailService(...)
let inbox = EmailInbox(...)

mailServie.inbox = inbox // dependency injection

Service Locator - это специальный объект (или фасад) у которого вы всегда сможете спросить нужную зависимость. Как правило, он либо знает где ее взять (на то он и locator), либо он сам хранит их у себя (см. паттерн реестр). Когда кому-то нужна какая-то зависимость, этот кто-то через механизмы singleton’а (или еще как) идет к Service Locator’у и спрашивает нужную зависимость.

class MailService
{
    var inbox: Inbox = ServiceLocator.shared.inbox // service locator
    ...
}

Третий термин (Inversion of Control) рассмотрим подробнее.

Но сначала давайте разберемся с тем, что за “control” инвертируется. Когда-то давно, когда приложения были консольными, control-flow приложения выглядел примерно так: мы запрашиваем с устройства ввода у пользователя данные, обрабатываем их и выводим на устройство вывода. Потоком управления управляло (пардон за тавталогию) само приложение. Когда появились графические UI приложение вместо того, чтобы управлять циклом приложения стало обрабатывать событие, отдав управление операционной системе. Таким образом получилась инверсия control-flow приложения. Таким же образом обстоит дело с инверсией при внедрении зависимостей при использовании паттерна Inversion of Control - управление временем жизни объекта и внедрением зависимостей делегируется специальному объекту, в задачу которого входит создание всех необходимых объектов и установка ссылок между ними. Этот специальный объект может называться сборщиком (assembler), контейнером (inversion of control container, см. Spring Framework) или еще как-нибудь, сути его это не меняеет.

class IoCContainer // Inversion of Control container (assembler)
{
    let mailService: MailService!
    let inbox: Inbox

    func build()
    {
        create()
        internlink()
        post()
    }

    private func create() {
        mailService = MailService()
        inbox = EmailInbox(...)
    }

    private func interlink() {
        mailService.inbox = inbox
    }

    private func post() {
        mailService.postCreate()
    }
}

Главное отличие контейнера от сервис локатора в том, что контейнер конкретный класс как правило не видит - зависимости для него появляются автомагически (automagically), а service locator видит и даже может быть хранит на него ссылку, а если еще и сильную… Кроме того, при использовании контейнера вы можете например иметь циклическую ссылку (с учетом конечно всех требований ARC и weak), или выполнить что-то после того, как весь граф (не дерево! и это важно) зависимостей будет собран (см. IoCContainer.post)