Skip to content

Latest commit

 

History

History
168 lines (119 loc) · 15.9 KB

about_dependency_injection.md

File metadata and controls

168 lines (119 loc) · 15.9 KB

О внедрении зависимостей

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

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

Зачем и когда стоит использовать внедрение зависимостей?

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

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

Давайте рассмотрим несколько примеров

Пример 1

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

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

Проект начал развиваться. Код сильно увеличился и стал насчитывать тысячи экранов и тысячи различной и не всегда простой бизнес логики.

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

В силу чего принимается решение - переходим с https запросов, на сокет.

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

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

Пример 2

Все та же команда, тот же продукт, но решение об архитектуре было принято другое. Теперь БД и сетевые запросы находятся на самом верхнем слое, как и UI, а внизу лежат абстракции, которые описывают бизнес логику, максимально не привязанную к технологии.

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

Одно из таких решение - использование сокет соединений.

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

А теперь примеры с точки зрения внедрения зависимости

Пример 1

В примере 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) { ... }
}

Правда пользы он принесет отнюдь не много.

Пример 2

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

По этой самой причине в современных языках очень распространены интерфейсы/протоколы. Код во втором примере начинает выглядеть так:

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".

А о самой библиотеке вы можете почитать в быстрый старт.