Поиск…


Инъекция зависимостей с контроллерами просмотра

Вводная инъекция Dependenct

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

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

Не следует путать с приведенным выше определением - инъекция зависимости просто означает предоставление объекту своих переменных экземпляра.

Это так просто, но это дает много преимуществ:

  • проще протестировать ваш код (используя автоматические тесты, такие как тесты на единицу и UI)
  • при использовании в тандеме с программно-ориентированным программированием упрощает изменение реализации определенного класса - проще рефакторировать
  • он делает код более модульным и многоразовым

Существуют три наиболее часто используемых способа: Инъекция зависимостей (DI) может быть реализована в приложении:

  1. Ввод инициализатора
  2. Впрыск недвижимости
  3. Использование сторонних инфраструктур DI (например, Swinject, Cleanse, Dip или Typhoon)

Есть интересная статья со ссылками на другие статьи о Injection Dependency, поэтому проверьте, хотите ли вы углубиться в принцип DI и инверсии управления.

Давайте покажем, как использовать DI с контроллерами View - ежедневную задачу для среднего разработчика iOS.

Пример без DI

У нас будет два контроллера просмотра: LoginViewController и TimelineViewController . LoginViewController используется для входа в систему и после успешного завершения, он переключится на TimelineViewController. Оба контроллера просмотра зависят от FirebaseNetworkService .

LoginViewController

class LoginViewController: UIViewController {

    var networkService = FirebaseNetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

TimelineViewController

class TimelineViewController: UIViewController {

    var networkService = FirebaseNetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func logoutButtonPressed(_ sender: UIButton) {
        networkService.logutCurrentUser()
    }
}

FirebaseNetworkService

class FirebaseNetworkService {

    func loginUser(username: String, passwordHash: String) {
        // Implementation not important for this example
    }
    
    func logutCurrentUser() {
        // Implementation not important for this example
    }
}

Этот пример очень прост, но предположим, что у вас есть 10 или 15 различных контроллеров представления, а некоторые из них также зависят от FirebaseNetworkService. В какой-то момент вы хотите изменить Firebase в качестве своего вспомогательного сервиса с помощью внутренней поддержки вашей компании. Для этого вам нужно будет пройти через каждый контроллер представления и изменить FirebaseNetworkService с помощью CompanyNetworkService. И если некоторые из методов в CompanyNetworkService изменились, у вас будет много работы.

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

Давайте перепишем этот пример и добавим Network Service в наши контроллеры представлений.

Пример с инъекцией зависимостей

Чтобы максимально использовать Injection Dependency Injection, давайте определим функциональность сетевой службы в протоколе. Таким образом, контроллеры представлений, зависящие от сетевой службы, даже не должны знать о реальной ее реализации.

protocol NetworkService {
    func loginUser(username: String, passwordHash: String)
    func logutCurrentUser()
}

Добавьте реализацию протокола NetworkService:

class FirebaseNetworkServiceImpl: NetworkService {
    func loginUser(username: String, passwordHash: String) {
        // Firebase implementation
    }
    
    func logutCurrentUser() {
        // Firebase implementation
    }
}

Давайте изменим LoginViewController и TimelineViewController для использования нового протокола NetworkService вместо FirebaseNetworkService.

LoginViewController

class LoginViewController: UIViewController {

    // No need to initialize it here since an implementation
    // of the NetworkService protocol will be injected
    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

TimelineViewController

class TimelineViewController: UIViewController {

    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func logoutButtonPressed(_ sender: UIButton) {
        networkService?.logutCurrentUser()
    }
}

Теперь встает вопрос: как мы вставляем правильную реализацию NetworkService в LoginViewController и TimelineViewController?

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

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // This logic will be different based on your project's structure or whether
    // you have a navigation controller or tab bar controller for your starting view controller
    if let loginVC = window?.rootViewController as? LoginViewController {
        loginVC.networkService = FirebaseNetworkServiceImpl()
    }
    return true
}

В AppDelegate мы просто берем ссылку на первый контроллер представления (LoginViewController) и вводим реализацию NetworkService с использованием метода впрыска свойств.

Теперь следующая задача - внедрить реализацию NetworkService в TimelineViewController. Самый простой способ - сделать это, когда LoginViewController переходит к TimlineViewController.

Мы добавим код инъекции в метод prepareForSegue в LoginViewController (если вы используете другой подход для навигации по контроллерам представлений, поместите там код для инъекций).

Теперь класс LoginViewController выглядит следующим образом:

class LoginViewController: UIViewController {
    // No need to initialize it here since an implementation
    // of the NetworkService protocol will be injected
    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "TimelineViewController" {
            if let timelineVC = segue.destination as? TimelineViewController {
                // Injecting the NetworkService implementation
                timelineVC.networkService = networkService
            }
        }
    }
}

Мы закончили, и все так просто.

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

Добавить класс реализации NetworkService:

class CompanyNetworkServiceImpl: NetworkService {
    func loginUser(username: String, passwordHash: String) {
        // Company API implementation
    }
    
    func logutCurrentUser() {
        // Company API implementation
    }
}

Переключите FirebaseNetworkServiceImpl с новой реализацией в AppDelegate:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // This logic will be different based on your project's structure or whether
        // you have a navigation controller or tab bar controller for your starting view controller
        if let loginVC = window?.rootViewController as? LoginViewController {
            loginVC.networkService = CompanyNetworkServiceImpl()
        }
        return true
    }

Вот и все, мы включили всю подкладочную реализацию протокола NetworkService, даже не коснувшись LoginViewController или TimelineViewController.

Поскольку это простой пример, вы можете не увидеть все преимущества прямо сейчас, но если вы попытаетесь использовать DI в своих проектах, вы увидите преимущества и всегда будете использовать Injection Dependency.

Типы инъекций зависимостей

В этом примере будет показано, как использовать шаблон проектирования зависимостей ( DI ) в Swift, используя следующие методы:

  1. Инициализатор Инъекции (правильный термин - Инъекция конструктора, но поскольку Swift имеет инициализаторы, он называется инициализационной инъекцией)
  2. Инъекция недвижимости
  3. Метод инъекции

Пример настройки без DI

    protocol Engine {
        func startEngine()
        func stopEngine()
    }
    
    class TrainEngine: Engine {
        func startEngine() {
            print("Engine started")
        }
        
        func stopEngine() {
            print("Engine stopped")
        }
    }
    
    protocol TrainCar {
    var numberOfSeats: Int { get }
    func attachCar(attach: Bool)
}

class RestaurantCar: TrainCar {
    var numberOfSeats: Int {
        get {
            return 30
        }
    }
    func attachCar(attach: Bool) {
        print("Attach car")
    }
}

class PassengerCar: TrainCar {
    var numberOfSeats: Int {
        get {
            return 50
        }
    }
    func attachCar(attach: Bool) {
        print("Attach car")
    }
}
    
class Train {
    let engine: Engine?
    var mainCar: TrainCar?
}

Инъекция зависимостей инициализатора

Как видно из названия, все зависимости вводятся через инициализатор класса. Чтобы вводить зависимости через инициализатор, мы добавим инициализатор в класс Train .

Класс Train теперь выглядит следующим образом:

class Train {
    let engine: Engine?
    var mainCar: TrainCar?
    
    init(engine: Engine) {
        self.engine = engine
    }
}

Когда мы хотим создать экземпляр класса Train, мы будем использовать инициализатор для инъекции конкретной реализации Engine:

let train = Train(engine: TrainEngine())

ПРИМЕЧАНИЕ . Основным преимуществом инъекции инициализатора по сравнению с введением свойств является то, что мы можем установить переменную как частную переменную или даже сделать ее константой с ключевым словом let (как это было в нашем примере). Таким образом, мы можем убедиться, что никто не может получить к нему доступ или изменить его.

Свойства Инъекции зависимостей

Использование свойств DI еще проще, если использовать инициализатор. Давайте введем зависимость PassengerCar к объекту поезда, который мы уже создали, используя свойства DI:

train.mainCar = PassengerCar()

Вот и все. Наш mainCar нашего поезда теперь является экземпляром PassengerCar .

Инъекция зависимостей метода

Этот тип инъекций зависимостей немного отличается от предыдущих двух, поскольку он не будет влиять на весь объект, но он будет вводить только зависимость, которая будет использоваться в рамках одного конкретного метода. Когда зависимость используется только в одном методе, обычно нехорошо вносить в нее весь объект. Давайте добавим новый метод в класс Train:

func reparkCar(trainCar: TrainCar) {
    trainCar.attachCar(attach: true)
    engine?.startEngine()
    engine?.stopEngine()
    trainCar.attachCar(attach: false)
}

Теперь, если мы назовем метод класса нового поезда, мы будем вводить TrainCar используя инъекцию зависимости метода.

train.reparkCar(trainCar: RestaurantCar())


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow