Модульность — актуальная тема современных разработчиков. Благодаря ей повышаются эффективность и удобство восприятия, уменьшается связанность, так что над кодовой базой одновременно работают разные команды. Однако, применяя модульную организацию систем, многие разработчики попадают в ловушку вертикальных архитектур.
Рассмотрим систему из трех отдельных модулей:
- Модуль 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
} // ...
}
С этим изменением архитектура стала полностью горизонтальной, разработка эффективно оптимизирована при сохранении целостности и масштабируемости системы:
Читайте также:
- Использование стека навигации SwiftUI для идеального поведения TabView
- Вопросы для собеседования iOS — Swift. Часть 2
- Swift: 7 секретов оптимизации
Читайте нас в Telegram, VK и Дзен
Перевод статьи Charles Prado: Using Dependency Injection to Build Elegant Horizontal Architectures