Получение графа одна из важных особенностей этой библиотеки. Так как возможность получать граф, позволяет его проверить на корректность. Тема проверки графа раскрыта в отдельной главе, а в этой маленькой главе остановимся на том, как получить граф, и какую информацию он хранит.
Для получения графа зависимостей у контейнера нужно вызвать функцию makeGraph()
которая быстро сконвертирует внутренний формат хранения данных в удобный для использования. Функция возвращает тип DIGraph
:
let graph: DIGraph = container.makeGraph()
Граф создается для текущего состояния, и автоматически не меняется в случае регистрации дополнительных зависимостей.
Граф представлен списком смежности и имеет следующую структуру:
typealias AdjacencyList = [[(edge: DIEdge, toIndices: [Int])]]
let vertices: [DIVertex]
let adjacencyList: AdjacencyList
При этом гарантируется, что количество вершин равно количествую элементов списка смежности.
Вершины представлены в виде массива, и к ним можно и нужно обращаться по индексу.
Список смежности представлен как массив, где каждый элемент соответствует вершине. Для каждого элемента в массиве содержится еще один массив - список ребер. При этом видно, что ребра представлены не в типичном виде - есть само ребро, и отдельно массив индексов куда возможен переход. Оно описано как картёж для удобства использования. Да по одному ребру возможен переход в несколько вершин. Это сделано специально ради возможности правильно описать модификатор many.
если вы не используете many и ваш граф корректен, то в массиве всегда будет один элемент.
Если нужно обойти весь граф, начиная с некоторой вершины, то достаточно воспользоваться adjacencyList
и использовать toIndices
.
Например, если мы хотим с помощью обхода в ширину обойти все достижимые вершины, начиная с некоторой, то можно написать следующую функцию:
let graph: DIGraph = container.makeGraph()
func bfs(from startIndex: Int) {
var visited: Set<Int> = []
var stack: [Int] = [startIndex]
while let fromIndex = stack.first {
stack.removeFirst()
visited.insert(fromIndex)
for toIndex in graph.adjacencyList[fromIndex].flatMap({ $0.toIndices }) {
if !visited.contains(toIndex) {
stack.append(toIndex)
}
}
}
return visited
}
Функция при этом возвращает все достижимые вершины из заданной.
Но просто бегать по графу не интересно - давайте разбираться какие данные хранятся в нем помимо списка смежности.
Вершина графа. Вершина графа может быть трех видов: компонент, аргумент, неизвестный тип:
enum DIVertex: Hashable {
case component(DIComponentVertex)
case argument(DIArgumentVertex)
case unknown(DIUnknownVertex)
}
Компоненты берутся из container-а и находятся вначале списка вершин. Аргументы и неизвестный тип находятся дальше, но между ними порядка нет.
Аргумент создается каждый раз, когда в компоненте встречается внедрение аргумента. Даже если типы у аргументов совпадают, то это будет две разных вершины.
Неизвестный тип создается каждый раз, когда для внедрения не удалось найти подходящий компонент. Даже если типы у неизвестного типа совпадают, то это будет две разных вершины.
Аргумент и неизвестный тип имеют одинаковую структуру:
struct DIArgumentVertex/DIUnknownVertex: Hashable {
let type: DIAType
}
И просто хранят в себе обычный Swift тип.
Вершина графа представленная компонентом. Содержит описание регистрации компонента. Уникальность обеспечивается компонентом - на каждую регистрацию в коде приходится один компонент и одна вершина. Их можно скопировать, но они будут равны.
Структура:
let componentInfo: DIComponentInfo
let lifeTime: DILifeTime
let priority: DIComponentPriority
let canInitialize: Bool
let alternativeTypes: [ComponentAlternativeType]
let framework: DIFramework.Type?
let part: DIPart.Type?
По порядку:
- Описание компонента с точки зрения уникальности и расположения - это регистрируемый тип, файл в котором происходит регистрация и строчка кода.
- Время жизни - тоже самое что было указано при регистрации. и также приоритет который меняется в случае если при регистрации была вызвана функция
default()
илиtest()
- Флаг, говорящий о том можно ли компонент инициализировать, или можно только внедрять в него зависимости по средствам функции
container.inject(into:...
. Компонент нельзя инициализировать, если не был передан метод инициализации, то есть было написано так:container.register(Type.self)
где самое важное это.self
- Альтернативные типы. Это типы, которые были записаны с помощью функции
as
. Так как альтернативные типы позволяют указать не только тип, но и тэг или имя в паре с типом, то тут альтернативные типы являются перечислением: просто тип, тип+тэг, тип+имя. - Фреймворк и часть, в которых происходила регистрация. Эта информация очень полезная, если у вас модульное приложение.
На этом описание вершины закончилось, но в дальнейшем возможны изменения/дополнения информации о компоненте.
Информация о ребре графа, или по другому о зависимости. Имеет следующую структуру:
let initial: Bool
let cycle: Bool
let optional: Bool
let many: Bool
let delayed: Bool
let tags: [DITag]
let name: String?
let type: DIAType
По порядку:
- initial - говорит о том, что данная зависимость идет из метода инициализации, а не отдельным внедрением.
- cycle - говорит о том, что есть указание разрыва цикла. Ну или по другому в коде было написано:
.injection(cycle: true...
. Если initial истина, то cycle точно лож. - optional - является ли данная зависимость опциональной. Это возможно если внедряемый тип имеет обычный опционал с точки зрения языка.
- many - является ли данная зависимость множественным внедрением.
- delayed - является ли данная зависимость отложенной. То есть используется или Lazy или Provider.
- tags - массив тэгов, по которым производился поиск компонента. Подробней про тэги.
- name - использовалось ли указание дополнительного имени. В отличие от тэгов имя может быть только одно, и не рекомендуемо к использованию.
- type - базовый! тип используемый при поиске куда указывает ребро. Этот тип не содержит информации об опциональности, тэгах и т.п. типах - только база.
Помимо просмотра графа можно также найти все циклы в графе и получить их.
Для этого у созданного графа надо вызвать функцию findCycles()
, которая найдет все циклы и вернет их в виде массива содержащего DICycle
который имеет следующую структуру:
let vertexIndices: [Int]
let edges: [DIEdge]
Длина массива индексов вершин и длина массива ребер всегда равные. Для каждой вершины есть соответствующее ребро которое хранит информацию о переходе из этого ребра в другое. Схематично это выглядит так: vertexIndices[i] -> edges[i] -> vertexIndices[( i + 1) % count]
Что означает, что по i ребру происходит переход из i вершины в i+1 вершину или начало.
Библиотека, в свою очередь, используя всю эту информацию, предлагает такую возможность как проверка графа зависимостей. С этой возможностью я настоятельно рекомендую ознакомиться, так как она может сэкономить уйму времени при разработке, и уменьшить количество ошибок во время исполнения.