Инверсия зависимостей - принцип SOLID, используемый для уменьшения связанности в компьютерных программах. Смысл его предельно прост:
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
В теории все звучит хорошо, но как дела обстоят на самом деле? Если мы говорим об ООП программах, то в них обязательно присутствует понятие объекта и класса. Если перефразировать все выше сказанное в терминах ООП программы на языке Swift то получим:
- Не стоит внутри одного объекта создавать другие объекты
- Любые объекты должны попадать в объект из вне
- Класс должен знать только о протоколах, и ничего не знать о других классах
- Протокол отвечает на вопрос "что нужно?", а не "что я умею?" - то есть протоколы создаются для того, чтобы класс мог запросить у программы данные, а не чтобы похвастаться программе как он умеет.
Для наглядности приведу пример:
class Vehicle {
func move() {
let engine: EngineForVehicle = EngineFabric.new()
position += speed * time
speed += engine.acceleration
}
}class Vehicle {
protocol EngineProtocol {
var acceleration: Double
}
let engine: EngineProtocol
init(engine: EngineProtocol) {
self.engine = engine
}
func move() {
position += speed * time
speed += engine.acceleration
}
}В первом случае Vehicle внутри своего метода запрашивает у фабрики конкретный двигатель, который обладает большим количеством свойств, большая часть которых ему не нужны. Во втором случае Vehicle получает на вход любой двигатель, который имеет лишь те свойства, которые нужны Vehicle и ничего большего.
Но это надуманный пример, слабо относящийся к реальности. Давайте рассмотрим пример части iOS приложения: Плохо:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let data: Data = Server.default.get("data")
let viewData: ViewData1 = ViewData1Converter().convert(data)
show(viewData)
}
}Хорошо:
class ViewController: UIViewController {
var server: ServerProtocol!
var converter: ConverterProtocol!
override func viewDidLoad() {
super.viewDidLoad()
let data: Data = server.get("data")
let viewData: ViewData1 = converter.convert(data)
show(viewData)
}
}Как видим, в первом случае программа обращаемся к реализации сервера и создаем конвертер. При данном подходе очень сложно будет протестировать части системы отдельно. Более того, части системы будет сложно заменить, так как код типа: Server.default будет разбросан по всему проекту.
Но есть еще один аргумент в пользу DI, который нигде не пишется, но я считаю его весомым - указание внутри класса нужных ему зависимостей происходит отдельно от их внедрения. То есть происходит деление обязанностей на две части:
- Классы делают только то, что должны делать, не заботясь о зависимостях
- Классы, отвечающие за внедрение зависимостей, занимаются только этим и по ним можно легко понять, как взаимодействуют части системы между собой
DITranquillity являет проектом с открытым исходным кодом, поддерживающим cocoapods и carthage.
Для подключения с помощью cocoapods, укажите в вашем podfile:
pod 'DITranquillity'
Для подключения через carthage, укажите в вашем cartfile:
github "ivlevAstef/DITranquillity"
Для более подробной информации, читайте документацию к cocoapods и carthage.
Во время регистрации, происходит объявление связей внутри нашей системы.
При регистрации, создается компонент. Компонент - это единица регистрации, или вся информация которую вы напишите в коде регистрации. В качестве имени компонента выступает тип или тип+имя, тип+тег. Внутри себя компонент хранит информацию о том, как создавать экземпляр объекта, какие компоненты в него внедрять, время жизни создаваемого объекта.
Чтобы в системе создать компонент, нужно:
- Создать контейнер
let container = DIContainer()- Прописать код регистраций
container.register{ Cat(name: "Felix") }
container.register{ Dog(name: "Buddy") }
container.register{ Home(animals: [$0 as Cat, $1 as Dog]) }- Провалидировать контейнер. Эта стадия является опциональной, и не рекомендуется ее запускать в релизной сборке
if !container.valid() {
fatalError("validation failed")
}Ура. После таких не сложных действий, мы зарегистрировали 3 компоненты, доступных по типам: Dog, Cat, Home.
Во время разрешения зависимостей, библиотека находит нужный компонент, и на основании него создает объект с внедрением указанных зависимостей.
Чтобы разрешить зависимости, нужно у контейнера спросить интересующий тип (или указать полное имя):
let cat: Cat = container.resolve()
let dog: Dog = container.resolve()
let home: Home = *container // упрощенный синтаксис
print(cat.name) // Felix
print(dog.name) // Buddy
print(home.animals) // [Cat, Dog]
print(home.animals.map{ $0.name }) // [Felix, Buddy]Более подробную информацию можно прочитать в следующих главах: