Skip to content

Latest commit

 

History

History
175 lines (149 loc) · 15.7 KB

injection.md

File metadata and controls

175 lines (149 loc) · 15.7 KB

Внедрение

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

Внедрение через метод инициализации

Объявление внедрения через метод инициализации происходит при регистрации нового компонента. Рассмотрим пример:

/// объявляем классы и протоколы
protocol Engine {}
protocol Wheel {}
protocol Body {}

class Car {
	private let engine: Engine
	private let wheel: Wheel
	private let body: Body
	init(engine: Engine, wheel: Wheel, body: Body) {
		self.engine = engine
		self.wheel = wheel
		self.body = body
	}
}

/// регистрация в контейнер.
container.register(Car.init)
container.register(Car.init(engine:wheel:body:))
container.register { 
	return Car(engine: $0, wheel: $1, body: $2)
}

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

container.register(Car.init)

Простой способ регистрации. Работает если у класса есть единственный метод инициализации. Таким способом стоит пользоваться в двух случаях:

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

container.register(Car.init(engine:wheel:body:))

Предпочтительный способ регистрации. Работает в большинстве ситуаций, и не усложняет чтение кода. Такой способ регистрации не поддерживает модификаторы. Для этого есть третий.

container.register { Car(engine: $0, wheel: $1, body: $2) }

Универсальный способ регистрации. Работает во всех ситуациях, легко читаем, и хорошо поддерживает auto completion. Но имеет недостаток, в виде необходимости писать цифры, что не всегда может быть удобным. Основное достоинство - поддерживает работу с модификаторами, а также позволяет внедрять объекты, не зарегистрированные в контейнере. Пример использования, с многими возможностями:

container.register {
	/// Внедряем:
	/// двигатель не зарегистрированный в DI контейнере
	/// все колеса зарегистрированные в контейнере и соответствующие типу: Wheel
	/// каркас, соответствующий тегу BMWBody
	Car(engine: BMWEngine(), wheels: many($0), body: by(tag: BMWBody.self, $1))
}

container.register(Car.init, modificator: { arg($0) })

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

/// Внедряем:
/// двигатель не зарегистрированный в DI контейнере, и который нужно передать снаружи.
/// колеса и каркас, которые до этого где-то были зарегистрированы в DI контейнере.
container.register(Car.init) { arg($0) }

// Теперь можно создать машину, указав один аргумент, и он автоматически подставится при создании
let car: Car = container.resolve(args: BMWEngine())
}

Внедрение через свойства

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

  • Идеологическим - на проекте не принято внедрять через метод инициализации
  • Историческим - уже написано много кода, который долго править
  • Синтаксическим - наличие циклических зависимостей, или ViewController-ов создаваемых из xib/storyboard не дает возможности делать все с использованием методов инициализации.
  • Временным - написать конструктор и прописать присвоении переменных занимает больше времени, чем написать свойство.

При этом внедрение через свойство достаточно богато на синтаксис. Давайте рассмотрим все варианты:

class Car {
	var engine: Engine!
	var wheels: [Wheel] = []
}

container.register(Car.init)
	/// При создании зависимостей ручками
	.injection { car in car.engine = BMWEngine() }
	.injection { $0.engine = BMWEngine() }
	/// При получении из DI контейнера
	.injection { car, engine in car.engine = engine }
	.injection { $0.engine = $1 }
	.injection(\Car.engine)
	.injection(\.engine)
	/// с модификаторами
	.injection { car wheel in car.wheels = many(wheel) }
	.injection { $0.wheels = many($1) }
	.injection(\Car.wheels) { wheel in many(wheel) }
	.injection(\.wheels) { many($0) }
	/// При необходимости указать другой тип
	.injection { $0.engine = $1 as OtherEngine }
	.injection(\Car.engine) { $0 as OtherEngine }
	/// При наличии цикла все те же варианты, но с добавлением `cycle: true`
	.injection(cycle: true) { $0.engine = $1 }
	/// С указанием имени, все те же варианты, но с добавлением `name: "..."`
	.injection(name: "BMW") { $0.engine = $1 } /// deprecated(Лучше использовать модификаторы)

Скорей всего можно придумать еще комбинации, но все они вытекают из этих вариантов, которые на самом деле распадаются на 3:

  1. .injection(_ closure: @escaping (Impl) -> ()) Вариант, когда внедряемый объект создается на месте. Наиболее простой вариант, и редко используемый. Основное его использование или проставление общих констант (казалось бы, при чем тут DI?), или добавление в уже существующих проект DI. Второй вариант более интересен, так как если проект большой, а на нем нет DI, то скорей всего есть какой-то аналог. И данное внедрение хорошо подходит, для переписывания проекта частями.
  2. .injection(name: String? = nil, cycle: Bool = false, _ closure: @escaping (Impl, Property) -> ()) Старый вариант внедрения зависимостей, до swift4.0. Позволяет внедрить любой объект зарегистрированный в DI контейнере. Для чего нужен каждый параметр:
  • name - является устаревшим способом, замененным на модификаторы. Нужно в случае если для одного и того же типа, надо иметь несколько вариантов компонента. Например:
container.register(Car.init)
	.as(Car.self, name: "BMW")
	.injection { $0.engine = BMWEngine() }
container.register(Car.init)
	.as(Car.self, name: "Eclipse")
	.injection { $0.engine = EclipseEngine() }

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

!!! Причины отказа: строковые литералы не являются типо-безопасными, более того они подвержены опечаткам. На смену пришли тэги. Да в отличие от имени они требуют чуть больше действий, но они имеет намного больше возможностей: можно пользоваться при инициализации, типо-безопасны, проверка валидности на стадии валидации, наличие области видимости. Все эти плюсы, в моем понимании, перевешивают их единственный минус - очевидность использования.

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

!!! Стоит учесть: Если при регистрации указывается много внедрений через свойства, то все они будут внедряться строго в указанном порядке, за исключением циклических внедрений - их момент внедрения сложно пред угадать. Единственное что можно сказать наверняка - циклические внедрения будут внедряться после всех не циклических и также по порядку.

  • closure - метод описывающий способ внедрения. На вход принимает объект в который внедряемся и объект, который будет внедрен. Ваша задача присвоить объект, так как swift до 4 версии не умел это делать каким либо способом.

!!! Lifehack: Не рекомендуется, но на самом деле в closure можно делать любые другие действия. Но лучше для этих целей использовать отдельный метод postInit, который исполняется после всех внедрений.

  1. injection(name: String? = nil, cycle: Bool = false, _ keyPath: ReferenceWritableKeyPath<Impl, P>, _ modificator: @escaping (Property) -> P) Улучшенный вариант предыдущего доступный со swift4.0. улучшение касается как синтаксиса - он становится короче и понятней, так и возможностей: при таком внедрении свойство может иметь область видимости меньше, чем в прошлом случае:
class Car {
	/// Прошлое способ не имеет возможности изменить свойство при таком модификаторе доступа
	/// Начиная со swift4.0 благодаря keyPath достаточно только знать о свойстве, без возможности модифицировать
	private(set) var engine: Engine!
}

Во всех остальных отношениях этот способ эквивалентен предыдущему. Стоит только уточнить про наличие еще одного параметра modificator: modificator нужен для добавления модификаторов. В случае если нам нужно получить объект с использованием модификатора, к примеру many, то это можно легко дописать в конце, например как это было сделано тут:

.injection(\.wheels) { many($0) }

Внедрение через метод

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

container.register(Car.init)
	.injection { $0.setup(engine: $1, wheels: many($2), body: $3) }

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

После инициализации

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

container.register(Car.init)
	.injection(\.engine)
	.injection(\.wheels) { many($0) }
	.injection(\.body)
	.postInit { car in
		car.move(to: Moscow.location)
	}