The first part mainly introduces some theoretical content, including the problems of object oriented programming, the basic concept of protocol oriented and decision model. This article (below) presents and explains some of the examples of code that I use on a daily basis in conjunction with protocol-oriented thinking and Cocoa development.

This article is reprinted from Meow God :onevcat.com/2016/12/pop… For the purpose of conveying more information, the copyright of this article belongs to the original author or source organization.

In love – Use protocols in daily development

WWDC 2015 has a very good keynote on POP: #408 Protocol-oriented Programming in Swift. Apple engineers illustrate the idea of POP by using two examples: drawing charts and sorting. We can use POP to decouple the code and make it more reusable through composition. In #408, however, the content is more theoretical, and our daily app development is more about Cocoa frameworks. After looking at #408, we’ve been thinking, how do we apply the idea of POP to our daily development?

In this section we’ll look at a practical example of how POP can help us write better code.

Protocol-based network request

The Web request layer is an ideal place to practice POP. In the following examples, we will start from scratch by building a less than perfect network request and model layer in the simplest protocol-oriented way, which may contain some poor design and coupling, but is the easiest initial result to get. Then we will gradually work out where each part belongs and refactor it in a way that separates responsibilities. Finally, we will test this network request layer. From this example, I hope to design POP code with many good features, including type safety, decoupling, ease of testing, and good extensibility.

Talk is cheap, show me the code.

The preliminary implementation

The first thing we want to do is request a JSON from an API and then convert it to an instance available in Swift. As an example of the API is very simple, you can directly access api.onevcat.com/users/onevc… To view the return:

{"name":"onevcat","message":"Welcome to MDCC 16!" }Copy the code

We can create a new project and add user.swift as the model:

// User.swift import Foundation struct User { let name: String let message: String init? (data: Data) { guard let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return nil } guard let name = obj? ["name"] as? String else { return nil } guard let message = obj? ["message"] as? String else { return nil } self.name = name self.message = message } }Copy the code

User.init(data:) parses the input data (retrieved from the network request API) into a JSON object, then extracts name and message from it, and builds the User instance returned on behalf of the API.

Now let’s look at the interesting part, which is how to use POP to request data from the URL and generate the corresponding User. First, we can create a protocol to represent the request. For a request, we need to know its request path, HTTP method, required parameters and other information. The initial agreement might look something like this:

enum HTTPMethod: String {
    case GET
    case POST
}

protocol Request {
    var host: String { get }
    var path: String { get }
    
    var method: HTTPMethod { get }
    var parameter: [String: Any] { get }
}
Copy the code

Concatenating host and path gives us the API address we need to request. For simplicity, HTTPMethod now only includes GET and POST requests, and in our example, we will only use GET requests.

Now you can create a new UserRequest to implement the Request protocol:

struct UserRequest: Request {
    let name: String
    
    let host = "https://api.onevcat.com"
    var path: String {
        return "/users/(name)"
    }
    let method: HTTPMethod = .GET
    let parameter: [String: Any] = [:]
}
Copy the code

UserRequest has a name attribute with an undefined initial value, and the rest of the attributes are defined by the protocol. Because the requested parameter username name is passed through the URL, it is sufficient that parameter is an empty dictionary. With the protocol definition and a specific request that satisfies the definition, we now need to send the request. In order that any Request can be sent in the same way, we define the sent method on the Request protocol extension:

extension Request { func send(handler: @escaping (User?) -> Void) { // ... Implementation of send}}Copy the code

In the send(handler:) argument, we define the escapable (User?) -> Void, after the request completes, we call this handler method to inform the caller whether the request is complete or not, and if all is well, return a User instance, otherwise return nil.

We want the send method to be generic for all requests, so obviously the callback cannot be of type User. We can abstract the callback parameters by adding an association type to the Request protocol. At the end of the Request add:

protocol Request {
    ...
    associatedtype Response
}
Copy the code

Then in UserRequest, we also add the type definition accordingly to satisfy the protocol:

struct UserRequest: Request {
    ...
    typealias Response = User
}
Copy the code

Now let’s re-implement the send method, and now we can make send general by replacing the specific User with Response. Here we use URLSession to send requests:

extension Request { func send(handler: @escaping (Response?) -> Void) { let url = URL(string: host.appending(path))! var request = URLRequest(url: HttpMethod = method.rawValue // In the example, we do not need 'httpBody'. In practice, we may need to convert parameter to data // request.httpbody =... Let the task = URLSession. Shared. DataTask (with: request) {data, res, the error in/print/processing results (data)} task. The resume ()}}Copy the code

The API entry point can be obtained by concatenating the host and path. Based on this URL, the request is created, configured, generated, and sent to the data Task. All that remains is to convert the data in the callback to the appropriate object type and call handler to notify the external caller. For User we know we can use user.init (data:), but for general Response we don’t yet know how to model the data. In Request, we can define a parse(data:) method that requires an appropriate implementation for the specific type of protocol. Thus, the task of providing the transformation method is “delegated” to UserRequest:

protocol Request {
    ...
    associatedtype Response
    func parse(data: Data) -> Response?
}

struct UserRequest: Request {
    ...
    typealias Response = User
    func parse(data: Data) -> User? {
        return User(data: data)
    }
}
Copy the code

Now that we have the method to convert data to Response, we are ready to process the result of the request:

extension Request { func send(handler: @escaping (Response?) -> Void) { let url = URL(string: host.appending(path))! var request = URLRequest(url: HttpMethod = method.rawValue // In the example, we do not need 'httpBody'. In practice, we may need to convert parameter to data // request.httpbody =... let task = URLSession.shared.dataTask(with: request) { data, _, error in if let data = data, let res = parse(data: data) { DispatchQueue.main.async { handler(res) } } else { DispatchQueue.main.async { handler(nil) } } } task.resume() } }Copy the code

Now, let’s try requesting this API:

let request = UserRequest(name: "onevcat")
request.send { user in
    if let user = user {
        print("(user.message) from (user.name)")
    }
}

// Welcome to MDCC 16! from onevcat
Copy the code

Refactoring, separation of concerns

The requirements were met, but the above implementation was pretty bad. Let’s look at the current definition and extension of Request:

protocol Request { var host: String { get } var path: String { get } var method: HTTPMethod { get } var parameter: [String: Any] { get } associatedtype Response func parse(data: Data) -> Response? } extension Request { func send(handler: @escaping (Response?) -> Void) { ... }}Copy the code

The biggest problem here is that Request manages too many things. All a Request should do is define the entry to the Request and the type of response to expect, but now the Request not only defines the value of host, but also knows how to parse the data. Finally, the send method is tied to the URLSession implementation and exists as part of the Request. This is very unreasonable, because it means that we cannot update the way requests are sent without changing the request, and they are coupled together. This structure makes testing extremely difficult. You may need to intercept requests with stubs and mocks and then return constructed data, using NSURLProtocol, or introduce some third-party testing framework that adds a lot of complexity to the project. This might have been an option in objective-C, but in the new age of Swift, we have much better ways of doing this.

Let’s start refactoring the code and adding tests to it. First we separate send(handler:) from Request. We need a separate type to be responsible for sending requests. Here, based on POP development, we start by defining a protocol that can send requests:

protocol Client { func send(_ r: Request, handler: @escaping (Request.Response?) -> Void)} // Error compilingCopy the code

The above declaration is semantically clear, but since Request is a protocol with an associated type, it cannot be used as a separate type. We can only use it as a type constraint to restrict the input parameter Request. The correct declaration should be:

protocol Client {
    func send<T: Request>(_ r: T, handler: @escaping (T.Response?) -> Void)

    var host: String { get }
}
Copy the code

In addition to using the

generic approach, we also moved host from Request to the Client, where it is more appropriate. Now we can remove the Request extension containing send and create a new type to satisfy clients. As before, it will use URLSession to send the request:

struct URLSessionClient: Client {
    let host = "https://api.onevcat.com"
    
    func send<T: Request>(_ r: T, handler: @escaping (T.Response?) -> Void) {
        let url = URL(string: host.appending(r.path))!
        var request = URLRequest(url: url)
        request.httpMethod = r.method.rawValue
        
        let task = URLSession.shared.dataTask(with: request) {
            data, _, error in
            if let data = data, let res = r.parse(data: data) {
                DispatchQueue.main.async { handler(res) }
            } else {
                DispatchQueue.main.async { handler(nil) }
            }
        }
        task.resume()
    }
}
Copy the code

Now the part that sends the request is separated from the request itself, and we define the Client using the protocol. In addition to URLSessionClient, we can use any type to satisfy this protocol and send requests. The implementation of the network layer and the request itself are no longer relevant, and we will see the benefits of this later in our testing.

One problem with this implementation is the Request parse method. The request should not and does not need to know how to parse the resulting data; that job should be left to Response. And now we don’t have any qualification on Response. Next we’ll add a new protocol, and the types that satisfy this protocol will know how to convert a data to the actual type:

protocol Decodable {
    static func parse(data: Data) -> Self?
}
Copy the code

Decodable defines a static parse method. Now we need to place this restriction on the Request’s Response association type so that we can ensure that all responses can be parsed. The original Request parse declaration can also be removed:

Protocol Request {var path: String {get} var method: HTTPMethod {get} var parameter: [String: Any] { get } // associatedtype Response // func parse(data: Data) -> Response? associatedtype Response: Decodable }Copy the code

The final thing to do is to make the User Decodable, and modify the parse part of the URLSessionClient code above to use the parse method in Response:

extension User: Decodable { static func parse(data: Data) -> User? { return User(data: data) } } struct URLSessionClient: Client { func send<T: Request>(_ r: T, handler: @escaping (T.Response?) -> Void) { ... // if let data = data, let res = parse(data: data) { if let data = data, let res = T.Response.parse(data: data) { ... }}}Copy the code

Finally, clean up UserRequest’s host and parse, which are no longer needed, and you have a type-safe, uncoupled protocol-oriented network layer. To call UserRequest, we can write:

URLSessionClient().send(UserRequest(name: "onevcat")) { user in
    if let user = user {
        print("(user.message) from (user.name)")
    }
}
Copy the code

Of course, you can also add a singleton to URLSessionClient to reduce the overhead of creating requests, add a Promise invocation to requests, and so on. Organized by POP, these changes are natural and do not involve the rest of the request. You can add other API requests to the network layer in a similar way to the UserRequest type, defining only what is necessary for the request without worrying about touching the network implementation.

Network layer testing

Declaring clients as protocols gives us the added benefit of not being limited to using a particular technology (such as URLSession here) to implement network requests. With POP, you just define a protocol for sending requests, and you can easily use a mature third-party framework like AFNetworking or Alamofire to build concrete data and handle the underlying implementation of the request. We can even provide a set of “fake” responses to the request for testing. This is close in concept to the traditional stub & Mock approach, but much simpler and unambiguous to implement. So let’s see what we can do.

Let’s start by preparing a text file and adding it to the project’s test target as the content returned by the network request:

// filename: users:onevcat {"name":"Wei Wang", "message": "hello"}Copy the code

Next, you can create a new type that satisfies the Client protocol. But unlike URLSessionClient, this new type of send method does not actually create the request and send it to the server. What we need to verify during testing is that if the server responds correctly to a request as documented, then we should be able to get the correct model instance as well. So all this new Client needs to do is load the defined results from a local file and verify that the model instance is correct:

struct LocalFileClient: Client { func send<T : Request>(_ r: T, handler: @escaping (T.Response?) -> Void) { switch r.path { case "/users/onevcat": guard let fileURL = Bundle(for: ProtocolNetworkTests.self).url(forResource: "users:onevcat", withExtension: "") else { fatalError() } guard let data = try? Data(contentsOf: fileURL) else { fatalError() } handler(T.Response.parse(data: data)) default: FatalError ("Unknown path")}} // In order to satisfy the requirements of 'Client', let host = ""}Copy the code

LocalFileClient does a simple thing. It checks the path property of the input request. If it is/Users /onevcat(the request we need to test), it reads the predefined file from the bundle of the test and parses it as the return result. Then call handler. If we need to add tests for other requests, we can add a new case entry. Also, the part that loads local file resources should be written in a more general way, but since we’re just using an example here, we don’t need to worry too much.

With the help of LocalFileClient, it is now easy to test UserRequest:

func testUserRequest() { let client = LocalFileClient() client.send(UserRequest(name: "onevcat")) { user in XCTAssertNotNil(user) XCTAssertEqual(user! .name, "Wei Wang") } }Copy the code

In this way, we can test requests without relying on any third-party test libraries or using complex techniques such as URL proxying or run-time message forwarding. Keeping code and logic simple is critical to project maintenance and growth.

scalability

Because of the high degree of decoupling, this POP-based implementation offers relatively loose possibilities for code extension. As we mentioned earlier, you don’t have to implement a complete Client on your own. Instead, you can rely on the existing network request framework to implement the method of sending requests. That is, you can easily replace one request method in use with another without affecting the definition and use of the request. Similarly, in Response processing, we now define Decodable to parse the model in our own handwriting. We can also use any third-party JSON parsing library to help us quickly build model types, simply by implementing a method that converts Data to the corresponding model type.

If you’re interested in pop-style web requests and model parsing, take a look at the APIKit framework, which is at the heart of the approach shown in the example.

Companion – Use protocols to help improve code design

Through protocol-oriented programming, we can free ourselves from traditional inheritance and assemble programs in a more flexible way like building blocks. Each protocol focuses on its own functionality, and thanks in particular to protocol extensions, we can reduce the risk of shared state from classes and inheritance and make the code clearer.

A high degree of protocolization helps decouple, test, and extend, while using protocols with generics frees us from the hassle of dynamic calls and type conversions and keeps our code safe.

For questions

I noticed when I was watching the demo, you always write firstprotocolRather thanstructorclass. Should we all just define the protocol first when we practice POP?

I wrote Protocol directly because I already had a good idea of what I was going to do and wanted the presentation to run out of time. However, you may not be able to write a proper protocol definition in the first place. The recommendation is to start with a “rough” definition, as I did in the demo, and then continue refactoring to get to a final version. Of course, you can use a pen and paper to create an outline before you define and implement the protocol. Of course, there’s no rule that you need to define a protocol first, but you can always start with a generic type and then go back to see if protocol oriented is more appropriate when you find something in common or encounter a dilemma we mentioned earlier. This requires some POP experience.

With all the benefits of POP, do we need to stop being object oriented and switch to protocol oriented?

The answer may disappoint you. In our daily projects, Cocoa is still a framework with a strong OOP color. That said, OOP may not be abandoned for a while. However, POP can be “compatible” with OOP, and we have seen many examples of using POP to improve code design. It should also be added that POP is not a silver bullet, it has a bad side. The biggest problem is that protocols increase the level of abstraction in your code (as with class inheritance), especially if your protocol inherits from other protocols. After several layers of inheritance, it becomes difficult to satisfy the end protocol, and it is difficult to determine which protocol requirements a method satisfies. This can complicate your code quickly. If a protocol doesn’t describe a lot in common, or is quickly understood, it might be easier to use the basic type.

Thank you for your speech and I would like to ask you about the use of POP in your project

We used the concept of POP a lot in the project. The network request example in the demo above was taken from a real project and we found it very easy to write because the code was simple and it was easy for new people to come in and make the transition. In addition to the model layer, we also use some POP code in the View and View Controller layer, such as the NibCreatable to create the view from the NIB, Support NextPageLoadable for paging requests to tableView Controller, EmptyPage for displaying pages when empty list, and so on. Due to the limited time, it is impossible to explain them all, so I only choose a representative and not very complex network example. Each protocol actually makes our code, especially the View Controller, shorter and makes testing possible. We can say that our project has benefited a lot from POP, and we should continue to use it.

Recommended data

A few I think in POP practice is worth a look at the information, willing to further understanding of friends may wish to see.

  • Protocol-Oriented Programming in Swift – WWDC 15 #408
  • Protocols with Associated Types – @alexisgallagher
  • Protocol Oriented Programming in the Real World – @_matthewpalmer
  • Practical Protocol-Oriented-Programming – @natashatherobot
  • IOS development of various technical materials