[SWIFT] About DI (Dependency Injection) possible implementation that we are working on in sub iOS

I'm an iOS engineer at iXIT, @ branch10480. This time, I would like to write about "** DI (Dependency Injection) ** Possible implementation" supported by the product Sub that I am in charge of.

Dependency Injection: What is DI (Dependency Injection)?

This DI (Dependency Injection) is described in Wikipedia as follows.

When creating a program using DI, the relationship between components is described using an interface, and no specific component is specified. By using another component, an external file, etc., which component is specifically used, the dependency between the components can be thinned.

At first, "I don't describe specific components" and "I can thin the dependencies between components" didn't come to my mind, so I couldn't imagine what kind of case it would be useful for.

So I will write it with the intention of explaining it to myself at that time.

What is the benefit of implementing a DI-enabled implementation?

From the conclusion,

  1. ** You will be able to replace with test modules (modules that return test data, etc.) such as mock and stub **
  2. ** You don't have to actually prepare a development server or external service (Firebase, etc.) **
  3. ** You will be able to implement test code in your project **

Is an advantage.

I will make it possible to imagine with concrete examples

This time, let's take an example of the function to acquire passport data and display a message according to the number of passport data. The image looks like this.

img.001.png As a tentative specification

  1. Acquire data
  2. When the number of data items is 0, an alert "No data" is displayed.

Let's go with this.

Check the configuration that cannot be DI

I wrote an example of implementation when I am not aware of DI. First, let's look at the Passport object and API client definition.

///Passport class
class Passport {
    //Convert from JSON to Passport object array and return
    static func parse(from json: JSON) -> [Passport] {
        // ...
    }
}

///API client
class APIClient {
    static let apiServerBaseURL = "https://dev-server"

    ///Passport acquisition API
    static func getPassports(completion: @escaping ([Passport])->Void) {
        let url = URL(string: apiServerBaseURL + "/passport")

        //Data acquisition to development server
        // ...
        //Receive result JSON from server->Generate an array of Passport objects from JSON
        //For convenience, let the response JSON from the server be JSON
        let passports = Passport.parse(from: JSON)
        completion(passports)
    }
}

The processing on the View side of the person who calls next is as follows.

///View side
class ViewController: UIViewController {
    ///When the screen is loaded
    override func viewDidLoad() {
        super.viewDidLoad()

        APIClient.getPassports(completion: { passports in
            if passports.isEmpty {
                //"No data" alert display processing
            }
        })
    }
}

Problems with this configuration and fix policy

By now, the specifications should have been met.

However, this always calls the API server (https: // dev-server), so you need to have a test API server that works. It's difficult to develop if the API is still under development.

This situation is ** dependent on an external API server **.

From here, we will rearrange this dependent part so that it can be replaced with other dependent parts (** dependency injection: DI **).

Abstract module using protocol

Swift allows you to abstract modules using protocol. This time, I will abstract the data acquisition part.

As for the function of the data acquisition part, if the function of "acquiring passport information" can be provided, it seems that the requirements of this sample can be satisfied. Therefore, the protocol is decided in this way.

///Passport data provision protocol
protocol PassportProviderProtocol {
    func getPassports(completion: @escaping ([Passport])->Void)
}

** A light supplement to protocol ** protocol is a list of properties and methods that you can access when you put an object in a variable of the defined protocol type and try to use it from the outside.

Conversely, you cannot access anything other than what is written here from the outside, and you do not know what is going on inside. (In the example shown this time, it is not known from the outside whether the object included as PassportProviderProtocol is actually communicating with the server or just creating the object internally, but the getPassports () method Only the things that can be used are clear.)

Create an API client and mock to comply with this protocol. The APIClient class is described as : PassportProviderProtocol and adds a declaration that it complies with this protocol.

///API client
class APIClient: PassportProviderProtocol {
    static let apiServerBaseURL = "https://dev-server"

    ///Passport acquisition API
    static func getPassports(completion: @escaping ([Passport])->Void) {
        let url = URL(string: apiServerBaseURL + "/passport")

        //Data acquisition to development server
        // ...
        //Receive result JSON from server->Generate an array of Passport objects from JSON
        //For convenience, let the response JSON from the server be JSON
        let passports = Passport.parse(from: JSON)
        completion(passports)
    }
}

Let's make another mock for testing. This is also compliant with the PassportProviderProtocol.

///API client mock
class APIClientMock: PassportProviderProtocol {
    enum TestPattern {
        case empty
        case notEmpty
    }
    var testPattern: TestPattern = .empty

    ///Get a passport
    func getPassports(completion: @escaping ([Passport])->Void) {
        //Make test results here
        let passports: [Passport]
        switch testPattern {
        case .empty:
            passports = []
        case .notEmpty:
            passports = [Passport()]
        }
        completion(passports)
    }
}

Call image from View

Now you're ready to replace it! The implementation on the View side is like this.

///View side
class ViewController: UIViewController {
    var passportProvider: PassportProviderProtocol = APIClient()

    ///When the screen is loaded
    override func viewDidLoad() {
        super.viewDidLoad()

        passportsProvider.getPassports(completion: { passports in
            if passports.isEmpty {
                //"No data" alert display processing
            }
        })
    }
}

You can now replace the production APIClient object with the development / test APIClientMock object by doing a property of type PassportProviderProtocol``passportsProvider!

The usage image is like this.

let vc = ViewController()
vc.passportProvider = APIClientMock()   //When you want to replace with a test module
self.navigationController?.pushViewController(vc, animated: true)

Summary

This time I tried to explain DI. In fact, if you develop with this configuration, you can proceed with API development at the same time without being affected by the progress of server-side development.

In addition, since it becomes necessary to separate modules as protocols, I felt that it would be easier to be aware of the range of functions that depend on them. I haven't started implementing the test code yet, so I'll try to summarize it if I can put it into shape.

It's been a long time, but thank you for reading! Next time is @oswhk! Thank you.

Recommended Posts

About DI (Dependency Injection) possible implementation that we are working on in sub iOS
About "Dependency Injection" and "Inheritance" that are easy to understand when remembered together
About characters that are completed in method arguments