Szukaj…


Wstrzykiwanie zależności za pomocą kontrolerów widoku

Dependenct Injection Intro

Aplikacja składa się z wielu obiektów, które współpracują ze sobą. Obiekty zwykle zależą od innych obiektów do wykonania jakiegoś zadania. Kiedy obiekt jest odpowiedzialny za odwoływanie się do swoich zależności, prowadzi to do wysoce sprzężonego, trudnego do przetestowania i trudnego do zmiany kodu.

Wstrzykiwanie zależności to wzorzec projektowania oprogramowania, który implementuje odwrócenie kontroli w celu rozwiązania zależności. Zastrzyk przekazuje zależność do obiektu zależnego, który mógłby go użyć. Umożliwia to oddzielenie zależności klienta od jego zachowania, co pozwala na luźne powiązanie aplikacji.

Nie należy mylić z powyższą definicją - wstrzyknięcie zależności oznacza po prostu nadanie obiektowi zmiennych instancji.

To takie proste, ale zapewnia wiele korzyści:

  • łatwiej przetestować kod (za pomocą automatycznych testów, takich jak testy jednostkowe i interfejsu użytkownika)
  • w połączeniu z programowaniem zorientowanym na protokół ułatwia zmianę implementacji określonej klasy - łatwiej jest ją refaktoryzować
  • czyni kod bardziej modułowym i wielokrotnego użytku

Istnieją trzy najczęściej stosowane sposoby implementacji Dependency Injection (DI) w aplikacji:

  1. Wtrysk inicjalizacyjny
  2. Zastrzyk nieruchomości
  3. Korzystanie z frameworków DI innych firm (takich jak Swinject, Cleanse, Dip lub Typhoon)

Jest ciekawy artykuł z linkami do większej liczby artykułów na temat wstrzykiwania zależności, więc sprawdź go, jeśli chcesz głębiej zagłębić się w zasadę DI i Inversion of Control.

Pokażmy, jak używać DI z kontrolerami widoku - codzienne zadanie dla przeciętnego programisty iOS.

Przykład bez DI

Będziemy mieć dwa kontrolery widoku: LoginViewController i TimelineViewController . LoginViewController służy do logowania, a po udanym Loign przełączy się na TimelineViewController. Oba kontrolery widoku są zależne od 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
    }
}

Ten przykład jest bardzo prosty, ale załóżmy, że masz 10 lub 15 różnych kontrolerów widoku, a niektóre z nich zależą również od FirebaseNetworkService. W pewnym momencie chcesz zmienić Firebase jako usługę zaplecza w wewnętrznej usłudze zaplecza firmy. Aby to zrobić, musisz przejść przez każdy kontroler widoku i zmienić FirebaseNetworkService za pomocą CompanyNetworkService. A jeśli niektóre metody w CompanyNetworkService ulegną zmianie, będziesz miał dużo pracy do zrobienia.

Testy jednostkowe i interfejsu użytkownika nie są objęte zakresem tego przykładu, ale jeśli chcesz przetestować kontrolery widoku z ściśle powiązanymi zależnościami, byłoby to bardzo trudne.

Przepiszmy ten przykład i wstrzyknij usługę sieciową do naszych kontrolerów widoku.

Przykład z wtryskiem zależnym

Aby jak najlepiej wykorzystać Wstrzykiwanie zależności, zdefiniujmy funkcjonalność usługi sieciowej w protokole. W ten sposób kontrolery widoku zależne od usługi sieciowej nie będą musiały nawet wiedzieć o prawdziwej jej implementacji.

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

Dodaj implementację protokołu NetworkService:

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

Zmieńmy LoginViewController i TimelineViewController, aby używały nowego protokołu NetworkService zamiast 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()
    }
}

Teraz pytanie brzmi: jak wstrzyknąć poprawną implementację NetworkService w LoginViewController i TimelineViewController?

Ponieważ LoginViewController jest kontrolerem widoku początkowego i będzie wyświetlał się przy każdym uruchomieniu aplikacji, możemy wstrzyknąć wszystkie zależności w 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
}

W AppDelegate po prostu odwołujemy się do pierwszego kontrolera widoku (LoginViewController) i wprowadzamy implementację NetworkService przy użyciu metody wstrzykiwania właściwości.

Teraz kolejnym zadaniem jest wstrzyknięcie implementacji NetworkService do TimelineViewController. Najłatwiej jest to zrobić, gdy LoginViewController przechodzi do TimlineViewController.

Dodamy kod wstrzyknięcia do metody preparForSegue w LoginViewController (jeśli używasz innego podejścia do nawigacji między kontrolerami widoku, umieść tam kod wstrzyknięcia).

Nasza klasa LoginViewController wygląda teraz tak:

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
            }
        }
    }
}

Skończyliśmy i to takie proste.

Teraz wyobraź sobie, że chcemy zmienić naszą implementację NetworkService z Firebase na implementację zaplecza naszej niestandardowej firmy. Musielibyśmy tylko:

Dodaj nową klasę implementacji NetworkService:

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

Przełącz FirebaseNetworkServiceImpl dzięki nowej implementacji w 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
    }

To wszystko, zmieniliśmy całą implementację protokołu NetworkService na tworzenie podkładów, nawet bez dotykania LoginViewController lub TimelineViewController.

Ponieważ jest to prosty przykład, możesz nie zobaczyć wszystkich korzyści w tej chwili, ale jeśli spróbujesz użyć DI w swoich projektach, zobaczysz korzyści i zawsze będziesz używał Dependency Injection.

Typy wtrysku zależności

W tym przykładzie zademonstrowano sposób używania wzorca projektowego Dependency Injection ( DI ) w Swift przy użyciu następujących metod:

  1. Iniekcja inicjalizacyjna (właściwym terminem jest iniekcja konstruktora, ale ponieważ Swift ma inicjalizatory, nazywa się to iniekcją inicjalizacyjną)
  2. Zastrzyk nieruchomości
  3. Metoda iniekcji

Przykładowa konfiguracja bez 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?
}

Injection Dependency Injection

Jak sama nazwa wskazuje, wszystkie zależności są wstrzykiwane przez inicjator klasy. Aby wstrzyknąć zależności za pomocą inicjatora, dodamy go do klasy Train .

Klasa pociągu wygląda teraz tak:

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

Kiedy chcemy utworzyć instancję klasy Train, użyjemy inicjatora do wstrzyknięcia konkretnej implementacji silnika:

let train = Train(engine: TrainEngine())

UWAGA: Główną zaletą zastrzyku inicjalizującego w porównaniu do wstrzykiwania właściwości jest to, że możemy ustawić zmienną jako zmienną prywatną lub nawet ustawić ją jako stałą za pomocą słowa kluczowego let (tak jak w naszym przykładzie). W ten sposób możemy upewnić się, że nikt nie będzie mieć do niego dostępu ani go zmieniać.

Właściwości Wstrzykiwanie zależności

Korzystanie z właściwości DI jest jeszcze prostsze niż przy użyciu inicjatora. Wstrzyknijmy zależność PassengerCar do obiektu pociągu, który już utworzyliśmy za pomocą właściwości DI:

train.mainCar = PassengerCar()

Otóż to. mainCar naszego pociągu jest teraz instancją PassengerCar .

Zastrzyk zależny od metody

Ten typ wstrzykiwania zależności jest nieco inny niż poprzednie dwa, ponieważ nie wpłynie na cały obiekt, ale wstrzykuje tylko zależność do zastosowania w ramach jednej konkretnej metody. Gdy zależność jest używana tylko w jednej metodzie, zwykle nie jest dobrze uzależniać cały obiekt od niej. Dodajmy nową metodę do klasy Train:

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

Teraz, jeśli TrainCar nową metodę klasy Train, wstrzykniemy TrainCar za pomocą wstrzyknięcia zależności metody.

train.reparkCar(trainCar: RestaurantCar())


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow