Внедрение зависимостей это паттерн проектирования, при котором объекты создаются не сразу в классе, где они нужны, а создаются где-то за пределами класса, и передаются ему позже. Основное его предназначение, повысить гибкость системы, и еще сильнее разграничить ответственности, за счет возможности подменять зависимые объекты без необходимости изменения класса.
Но помимо гибкости/масштабируемости системы, этот паттерн повышает выразительность классов, так как в классе остается только необходимая логика, и информация о том, что нужно для реализации этой логики.
Как и любой паттерн, этот имеет определенную область применения. Не правильное использование паттерна, скорее навредит коду, нежели поможет. Под неправильным использованием я подразумеваю, как варианты когда внедрения совсем нет, так и варианты когда внедрение начинают использовать повсеместно.
При этом само понятие очень близко стоит с Инверсией управления и Принципом инверсии зависимостей. В каком-то смысле этот паттерн это практика, а принципы это теория. Сами принципы отлично описаны в различных источниках, но они будут задеты ниже в примерах.
Команда разработки принялась за разработку проекта, который точно должен просуществовать минимум 5 лет, а скорей всего и больше 10.
Архитекторы собрались, и решили, что БД и сетевые запросы будут в самом нижнем слое. Потом над ядром будет слой бизнес логики, а над ним слой UI.
Проект начал развиваться. Код сильно увеличился и стал насчитывать тысячи экранов и тысячи различной и не всегда простой бизнес логики.
В какой-то момент стало ясно - обычные https запросы не годятся для получения и отправки данных на сервер, так как очень много изменений происходит со стороны сервера. А если приложение начинает постоянно дергать сервер, с вопросом "изменилось ли что-нибудь?", то нагрузка на сервер и канал возрастает, и не позволяет получать обновления с желаемой скорость.
В силу чего принимается решение - переходим с https запросов, на сокет.
Но так как сетевые запросы находились в самом нижнем слое, то приходится перестраивать полностью все взаимодействие слой за слоем.
Пример далеко не вымышленный и такие или подобные ошибки часто совершаются на этапе проектирования. Причем не зависимо от того каким образом происходило внедрение зависимостей, проблема имела место быть.
Все та же команда, тот же продукт, но решение об архитектуре было принято другое. Теперь БД и сетевые запросы находятся на самом верхнем слое, как и UI, а внизу лежат абстракции, которые описывают бизнес логику, максимально не привязанную к технологии.
То есть был применен принцип инверсии зависимостей и инверсии управления. Теперь бизнес логика ничего не знает о технических ограничениях системы, а значит, пишется максимально близко к требованиям заказчика. А после её описания инженеры тратят время и силы, на то чтобы подогнать технические ограничения к необходимой бизнес логике.
Одно из таких решение - использование сокет соединений.
Но в отличие от прошлого варианта решения, на этот раз в идеале надо дополнить/заменить сетевой слой дополнительным функционалом. Да, с некоторой долей вероятности бизнес логику и UI тоже придется править, но это будет намного в меньших масштабах, и в основном не по причинам плохой архитектуры, а по причинам того, что часть абстракций было сделано исходя из технических ограничений.
В примере 1 архитектура сделана без каких либо инверсий. В такой архитектуре можно поставлять зависимости как из вне согласно паттерну внедрения зависимостей, так и прописывать все их прямо внутри классов, например:
class SomeUI {
let someUseCase = SomeUseCase()
}
class SomeUseCase {
let someNetwork = SomeNetwork()
let someDB = SomeDB()
}
Такой вариант, думаю многим знаком, правда если понадобиться заменить один тип сетевого соединения на другой, то придется не просто пересматривать абстракции, но и во всех местах переписывать инициализацию класса. Что в дальнейшем еще усугубляет положение, так как пример становиться похожим на:
class SomeUI {
let someUseCase = SomeUseCase.shared
}
class SomeUseCase {
let someNetwork = SomeNetwork.shared
let someDB = SomeDB.shared
}
Но даже в такой архитектуре, возможно, реализовать паттерн внедрения зависимостей если написать как-то так:
class SomeUI {
let someUseCase: SomeUseCase
init(useCase: SomeUseCase) { ... }
}
class SomeUseCase {
let someNetwork: SomeNetwork
let someDB: SomeDB
init(network: SomeNetwork, db: SomeDB) { ... }
}
Правда пользы он принесет отнюдь не много.
Во втором примере ситуация кардинально меняется, так как без паттерна внедрения зависимостей её сложно реализовать. Произошла инверсия зависимостей, из-за чего нельзя создать объект Базы данных или сетевой, так-как о его реализации бизнес логика ничего не знает.
По этой самой причине в современных языках очень распространены интерфейсы/протоколы. Код во втором примере начинает выглядеть так:
class SomeUI {
let someUseCase: SomeUseCase
init(useCase: SomeUseCase) { ... }
}
class SomeNetwork: SomeNetworkContract { ... }
class SomeDB: SomeDBContract { ... }
.............................................
protocol SomeNetworkContract { ... }
protocol SomeDBContract { ... }
class SomeUseCase {
let someNetwork: SomeNetworkContract
let someDB: SomeDBContract
init(network: SomeNetworkContract, db: SomeDBContract) { ... }
}
Я не просто так провел в примере черту - тем самым я обозначил, что контракты и бизнес логика находятся на одном уровне.
Почему я протоколы назвал контрактами? К сожалению, я не смог найти как обычно называются подобные сущности, поэтому скорей всего придумал свое название, но я постараюсь объяснить. В данном случае эти протоколы описывают контракт с системой, где они используются. А контракт это договор, который звучит так: Система предоставь мне реализацию вот таких протоколов, а я в замен дам тебе возможность использовать класс. А название "контракт" я стащил из контрактного программирования.
В таком варианте очень важно, чтобы контракты максимально точно описывали, то, что нужно бизнес логике, и абстрагировались от технических аспектов системы. То есть при написании контрактов надо просто забыть, что у вас есть База данных, или Сеть, и уж тем более забыть, что База данных это файл с ограничением на скорость записи - все это технические аспекты. Да и контракт тогда возможно нужен один:
protocol SomeDataSourceContract { ... }
protocol SomeUseCaseDelegate { ... }
class SomeUseCase {
let someDataSource: SomeDataSourceContract
weak var someDelegate: SomeUseCaseDelegate?
init(dataSource: SomeDataSourceContract) { ... }
}
Правда в примере я еще написал делегат, дабы приблизить вариант к боевым реалиям, да и показать, что Америку я тут не открываю, и подобный код вы можете видеть часто, если пишете код под экосистему Apple.
И тут ответ, казалось бы - используйте библиотеку и будет вам счастье, но перед этим я хочу показать, как на нашем примере может выглядеть код, если бы библиотек не было:
class Main {
func run() {
let network = SomeNetwork()
let db = SomeDB()
let dataSource = SomeDataSource(network: network, db: db)
let useCase = SomeUseCase(dataSource: dataSource)
let ui = SomeUI(useCase: useCase, nextUIMaker: {
let otherDataSource = OtherDataSource(network: network, db: db)
let otherUseCase = OtherUseCase(dataSource: otherDataSource)
let result = OtherUI(otherUseCase: otherUseCase)
otherUseCase.delegate = result
return result
})
useCase.delegate = ui
}
}
Да тут я слегка слукавил, и добавил возможность перехода на другой UI в максимально простой форме. Но в современных программах десятки, сотни, а то и тысячи экранов. Переходы между экранами достаточно запутаны, а на одном экране может присутствовать намного больше чем один бизнес сценарий, и скорей всего у вас точно будет больше чем один сетевой запрос.
И что делать в таких ситуация? Если этот код расширить до 3-5 экранов, и 5-8 бизнес требований, то в нем будет уже сложно ориентироваться.
И вот тогда когда у вас в программе все хорошо с инверсией, с абстракциями, много экранов, много бизнес требований, только в этом случае на помощь приходят библиотеки для внедрения зависимостей.
Библиотеки бывают разные - от простых, которые позволяют просто добавлять и получать объект, до сложных, которые способны следить за циклами, знают о времени жизни объекта, и интегрированных со средой/языком в котором вы пишите.
Они различаются и синтаксическими решениями: Кто-то считает, что способ внедрения зависимостей должен оставаться за рамками прикладного кода, кто-то в угоду уменьшения кода пишет информацию о внедрении прямо в прикладном коде в виде атрибутов, или каких либо других средств.
Так как я считаю, что способ внедрения зависимостей это техническое решение, а технические решения должны:
- Быть подменяемые
- Быть в тени, дабы гарантировать независимость кода от них
То и моя библиотека остается в тени.
Надеюсь, я вас убедил, что внедрение зависимостей это полезный и важный паттерн в программировании, ну и также что вам нужна библиотека для внедрения зависимостей, и эта библиотека "DITranqullity".
А о самой библиотеке вы можете почитать в быстрый старт.