vinhnx / notes

my today I learn (TIL) journal, includes everything that I found interesting, not necessarily relates to programming
43 stars 2 forks source link

[Swift] Generic NetworkRequest protocol with associated types for Moya (Alamofire), ObjectMapper #47

Open vinhnx opened 7 years ago

vinhnx commented 7 years ago

The problem

Say, our application uses Moya for network request and ObjectMapper for JSON mapping. This could lead to duplicated or complicated code.

(yes, I think Alamofire is already complex enough, and Moya is a wrapper on top of it).

The approach

My approach to solve this is that, we could declare a shared NetworkRequest protocol that also have associatetype like Moya's Target and ObjectMapper's Model.

I think associatedtype like placeholders, or template in other languages.

Protocol with associated types

For for usage example, we have two Google network managers, defined in GoogleManager class cluster:

both are from Google API.

We could conform them to NetworkRequest protocol, so they could have all traits we defined in the extensions.

This leads to better code, in my opinion.

Key things to remember πŸ”‘

The conformer just need to fill in associatedtype requirements we define in the conforming protocol.

The rest is automatically handled, since we write default protocol implementation in extension, so all conformers get those implementation for πŸ†“ (who doesn't want free stuffs πŸ˜‹ ?!)

For our case, basically NetworkRequest conformers all inherit this request method:

func request(from provider: MoyaProvider<Target>, target: Target, for type: Model.Type, completion: @escaping ModelCompletion)

Code sample

πŸ‘‰ NetworkRequest protocol with associatedtypes and extend/default implementation

import Foundation
import Moya
import ObjectMapper
import Moya_ObjectMapper

// Conformnace `Model` need to conform to MappableResponse that has these shared properties
protocol MappableResponse: Mappable {
    var valid: Bool { get }
    var errorMessage: String { get }
}

// Conformnace `Target` need to conform to TargetPlaceholder to provide generic enum to NetworkRequest
// request function
protocol TargetPlaceholder: TargetType {}

// Base network request
// Usage: conformer need to provide two informations.
// `Model`: model that request need to parse to
// `Target`: an TargetPlaceholder request route enum
protocol NetworkRequest {
    typealias ModelCompletion = (NVResult<Model>) -> Void

    associatedtype Target: TargetPlaceholder
    associatedtype Model: MappableResponse
}

extension NetworkRequest {

    // MARK: - Default implementation

    func request(from provider: MoyaProvider<Target>, target: Target, for type: Model.Type, completion: @escaping ModelCompletion) {

        log.debug(target.parameters)

        provider.request(target) { result in

            switch result {
            case .success(let response):
                do {
                    let mappedObject = try response.mapObject(type)
                    log.debug(mappedObject)

                    DispatchQueue.main.async {
                        // IMPORTANT: Should check if response is valid before dispatch completion result
                        if mappedObject.valid {
                            completion(NVResult.success(mappedObject))
                        } else {
                            errorResultHandler(message: mappedObject.errorMessage, completion: completion)
                        }
                    }

                } catch let error {
                    errorResultHandler(message: error.localizedDescription, completion: completion)
                }
            case .failure(let error):
                errorResultHandler(message: error.localizedDescription, completion: completion)
            }
        }
    }
}

extension NetworkRequest {

    // MARK: - Private

    private func errorResultHandler(message: String, completion: @escaping ModelCompletion) {
        log.error(message)

        DispatchQueue.main.async {
            let resultError = NVGeneralError.message(message)
            completion(NVResult.failure(resultError))
        }
    }
}

πŸ‘‰ GoogleManager class cluster

import Foundation

class GoogleNetworkManager: NetworkRequest {
    typealias GoogleDirectionParam = [String: String]
    typealias ModelResponseCompletion = (NVResult<Model>) -> Void

    // required: this is to fill placeholder requirements from NetworkRequest
    typealias Model = GoogleResponse
    typealias Target = GoogleTarget

    // MARK: - Public

    func direction(params: GoogleDirectionParam, completion: @escaping ModelResponseCompletion) {
        request(from: GoogleProvider, target: Target.direction(params: params), for: Model.self, completion: completion)
    }
} 

class GooglePlaceNetworkManager: NetworkRequest {
    typealias ModelResponseCompletion = (NVResult<Model>) -> Void

   // required: this is to fill placeholder requirements from NetworkRequest
    typealias Model = GooglePlaceResponse
    typealias Target = GoogleTarget

    // MARK: - Public

    func searchForhotelWithName(_ query: String, completion: @escaping ModelResponseCompletion) {
        guard query.isEmpty == false else { return }
        request(from: GoogleProvider, target: Target.hotelSearch(query: query), for: Model.self, completion: completion)
    }
}

Notes:


This apply to just Google network manager class, but in practice, we could just extend the same to all other new network classes.

I hope this useful (at least to my future self)~~!

πŸš€

vinhnx commented 7 years ago

If you are reading this, suggestions are welcome! πŸ™‡

vinhnx commented 6 years ago

Related https://github.com/vinhnx/iOS-notes/issues/135