After reading Moya, my first impressions of Moya are functional programming, ubiquitous protocol-oriented programming, and the idea of plug-ins.

The principles of Moya have been analyzed in detail in many articles, and this article focuses on the design of Moya that is worth learning from.


TargetType – Request parameter protocol

In the official example, we need to use a Provider to invoke the network request. Use the following code:

let provider = MoyaProvider<LoginAPI>()
provider.request(.login) { ... }
Copy the code

Very straightforward call, it is also the only entry to call Moya.

LoginAPI is an enumerated type that follows the TargetType protocol and is used to define the basic parameters and behaviors in network requests.

The TargetType protocol is as follows:

public protocol TargetType {
    var baseURL: URL { get }
    var path: String { get }
    var method: Method { get }
    var parameters: [String: AnyObject]? { get }
    var sampleData: Data { get}... }// Implement a target
enum LoginAPI {
  enum authenticate
  enum login
}
extension LoginAPI: TargetType {
  var baseURL: NSURL{ return URL(string: "http://foo.cn")}var path: String {
    switch self {
      case authenticate: return "/authenticate"
      case login: return "/login"}}... }Copy the code

The benefits of this approach are obvious. The parameters of network requests are defined entirely in a protocol-oriented way, and the use of enumerated types makes it easy to manage a set of apis. Properties use read-only modifications to ensure state control for value type programming.

The Provider takes the parameters from the LoginAPI as enumerated types and combines them into a Request, which is sent via Alamofire.

Provider constructors – Functional programming for ease of use and flexibility

Let’s look at the Provider constructor first:

public init(endpointClosure: @escaping EndpointClosure = Provider.defaultEndpointMapping,
            requestClosure: @escaping RequestClosure = Provider.defaultRequestMapping,
            stubClosure: @escaping StubClosure = Provider.neverStub,
            callbackQueue: DispatchQueue? = nil,
            manager: Manager = Manager.default,
            plugins: [PluginType] = [],
            trackInflights: Bool = false)
Copy the code

The constructor of a Provider contains multiple closures. A quick look at the usefulness of each parameter:

EndpointClosure: Provides a default implementation that returns either a TargetType initialized Endpoint object or a custom Endpoint.

RequestClosure: The Endpoint obtains a Request and sends network requests. You can also send customized requests.

StubClosure: Determine which stub policy is enabled, which can be enabled during unit testing.

Plugins, which are covered in more detail in the following article.

In summary, it’s not hard to see what providers really do:

Why define EndpointClosure and RequestClosure when TargetType already has all the parameters for a web request? This seemingly cumbersome design is actually designed to achieve the functionality of plug-ins.

First, the official recommendation is to implement TargetType as an enumeration type, and define all request parameters as read-only, so that for a request, its initial parameters will be uniquely determined, strictly controlling its state.

At the same time, Moya allows the user to pass processing logic through closure arguments during TargetType generation and Request generation by the Endpoint. EndpointClosure allows the user to modify the parameters of the original TargetType, such as temporarily adding an HTTPHeader or adding paging information at this stage. RequestClosure allows the user to have one last chance to process the URLRequest while it is being generated by the Endpoint. You can modify cookies, customize header fields, etc.

In fact, you can initialize the Provider without using any parameters, and the default implementation does all the conversion for you. The ability to insert corrective code in the constructor’s closure parameters makes it very flexible when the user needs it.

With no extra processing, users can create a Provider directly by ignoring all the arguments in the constructor, which greatly improves the ease of use of the constructor. Users can also insert custom processing logic into the constructor, providing enough flexibility.

Stub – A network layer that is easy to test

An important concept in unit testing is stubs: It may require a network request, a database query, or a lot of construction criteria. Therefore, we need to use stubs to provide the default data and return fake data directly or later when needed.

StubClosure in a Provider is a switch provided for unit tests. Users can return stub types as needed in stubClosure:

public enum StubBehavior {
    case never
    case immediate
    case delayed(seconds: TimeInterval)}Copy the code

The dummy data it returns is provided by sampleData in TargetType and Endpoint.

Providing unit test switches in constructors, while allowing fake data to be added to constructors, makes testing network requests for Moya surprisingly easy.

Plugin – The idea of plugins

The Plugin implements the Plugin. The Plugin parameters provided by the Provider are a collection of instances that conform to PluginType. It is primarily used for transaction processing at key points in network requests. Let’s take a look at the PluginType protocol:

public protocol PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
    func willSend(_ request: RequestType, target: TargetType)
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response.MoyaError>}Copy the code

It defines four nodes to invoke the plug-in. The following is a flow diagram of the invocation:

At each of the four Plugin nodes, Moya iterates through all plug-ins in the plugins parameter to execute their respective protocol methods.

Moya adds four plugins such as NetworkLoggerPlugin and NetworkActivityPlugin that can be used directly for logging and loading.

The plug-in acts as if it abstracts the proxy callbacks we normally use into protocols, but it’s much more scalable.

We can easily implement PluginType customization and implement the corresponding node methods, and other unimplemented node methods will call the default extension methods without causing any side effects. This approach makes the network layer more pure, focusing only on the high-level network abstraction, leaving the rest of the processing logic open to user customization in the form of plug-ins.

Cancellable – Single duty request cancellation protocol

Network requests need to be cancelled in many scenarios. For example: After exiting the current page, the current page for display data requests should be cancelled.

Moya provides a return value for each request function. The return value is of type Cancellable:

public protocol Cancellable {
    var isCancelled: Bool { get }
    func cancel(a)
}
Copy the code

It has only one read-only property and one cancel method. When customizing to follow the Cancellable protocol, it is recommended to declare the isCancelled property as:

public private(set) var isCancelled: Bool = false
Copy the code

You can ensure that the isCancelled property is changed only inside the cancel method and is read-only externally.

Calling Cancellable’s cancel method ends up calling alamofire.request.cancel (), but returning Cancellable makes the purpose of the type very clear. The return value is only used to cancel network requests, satisfying the single responsibility principle.

Result – Flexible error handling ideas

Moya relies on two open source libraries; in addition to Alamofire, it relies on Result.

Result is very simple, with only about 400 lines of code, and it provides an idea for error handling: return data if it is correct, return error type if it is wrong. Official example:

// Definition
typealias JSONObject = [String: Any]

enum JSONError: Error {
    case noSuchKey(String)
    case typeMismatch
}

func stringForKey(json: JSONObject, key: String) -> Result<String.JSONError> {
    guard let value = json[key] else {
        return .failure(.noSuchKey(key))
    }
    if let value = value as? String {
        return .success(value)
    }
    else { 
        return .failure(.typeMismatch)
    }
}

// Usage
switch stringForKey(json, key: "email") {
case let .success(email):
    print("The email is \(email)")
case let .failure(.noSuchKey(key)):
    print("\(key) is not a valid key")
case .failure(.typeMismatch):
    print("Didn't have the right type")}Copy the code

AFNetworking provides error and success callbacks to process the result of a request, but in practice it is common to execute code or share state after both error and success. If you use two callbacks, you must write two copies of code.

Unlike AFNetworking, Result returns correct or incorrect results all at once. Users can use the Switch statement to process results within a scope, which gives them more flexibility.

conclusion

Alamofire has become almost an official web framework, and the most popular web abstraction library on it is Moya. I think this is entirely due to the fact that it meets people’s needs for network abstraction layer: testable, easy to use, extensible.