Внедрение зависимостей для создания элегантных горизонтальных архитектур

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

Рассмотрим систему из трех отдельных модулей:

  • Модуль A пользовательского интерфейса с SoundView.
  • Модуль B компонентный с SoundButtonView.
  • Модуль C логический с классом SoundService.

SoundView модуля A зависит в этом сценарии от SoundButtonView модуля B, а тот  —  от SoundService модуля C. Так формируется вертикальная цепочка модулей с заметным усилением зависимости верхних, таких как A, от нижних  —  B и C. Изменения в нижестоящем модуле чреваты нарушением функциональности вышестоящих, из-за такой многоуровневой структуры зависимостей время компиляции увеличивается, освоение системы усложняется:

Вертикальная архитектура

Горизонтальная структура с внедрением зависимостей  —  решение поэлегантнее. При таком расположении модуль A не «знает» о B или C, и наоборот.

Преимущества такого подхода:

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

Горизонтальная структура строится так:

  • Создается один или несколько мостовых модулей DI с необходимыми протоколами, которые помещаются в модули A, B и C вместо конкретных реализаций:
// Расположение: модуль DI (несколько файлов)
protocol SoundViewProtocol { /* ... */ }
protocol SoundButtonViewProtocol { /* ... */ }
protocol SoundServiceProtocol { /* ... */ }
  • Ссылаясь на что-то из другого модуля, вместо конкретной реализации используем протокол:
// Расположение: модуль A
import DI

class SoundView: SoundViewProtocol {
var soundButtonView: SoundButtonViewProtocol?
// ...
}
// Расположение: модуль B
import DI

class SoundButtonView: SoundButtonViewProtocol {
var soundService: SoundServiceProtocol?
// ...
}
// Расположение: модуль C
import DI

class SoundService: SoundServiceProtocol {
// ...
}
  • На уровне приложения для разрешения зависимостей применяется локатор служб или инструмент вроде Swinject:
// Расположение: модуль DI
import Swinject

class AppDependencies {
let container = Container()

init() {
setupDependencies()
}

private func setupDependencies() {
container.register(SoundServiceProtocol.self) { _ in
SoundService()
}
container.register(SoundButtonViewProtocol.self) { r in
let service = r.resolve(SoundServiceProtocol.self)!
let buttonView = SoundButtonView()
buttonView.soundService = service
return buttonView
}
container.register(SoundViewProtocol.self) { r in
let buttonView = r.resolve(SoundButtonViewProtocol.self)!
let soundView = SoundView()
soundView.soundButtonView = buttonView
return soundView
}
}

func resolveSoundView() -> SoundViewProtocol {
return container.resolve(SoundViewProtocol.self)!
}
}
  • Зависимости в Swinject минимизируем созданием классов Container/Factory для каждого модуля, разрешая зависимости на уровне приложения:
// Расположение: модуль DI
protocol ModuleBFactoryProtocol {
func createSoundButton() -> SoundButtonProtocol
}
// Расположение: приложение
// Swinject предназначен исключительно для цели приложения. Поэтому модулям приложения
// импортировать его не нужно, чем облегчается будущее удаление или замена.
import Swinject

class ModuleBFactory {
func createSoundButton() -> SoundButtonProtocol {
let soundButton = container.resolve(SoundButtonProtocol.self) as! SoundButton
}
}
// Расположение: модуль A
import DI
// Точка входа — координатор, представление, контроллер и т. д.
class ModuleAEntryPoint {
// ...
init(moduleBFactory: ModuleBFactoryProtocol) {
self.moduleBFactory = moduleBFactory
} // ...
}

С этим изменением архитектура стала полностью горизонтальной, разработка эффективно оптимизирована при сохранении целостности и масштабируемости системы:

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Charles Prado: Using Dependency Injection to Build Elegant Horizontal Architectures

Предыдущая статьяПроект инженерии данных с DAG Airflow «от и до». Часть 1
Следующая статьяJoin-операции в MySQL — инструмент оптимизации поиска данных