面向协议编程(Protocol Oriented Programming,简称 POP)是 Swift 的一种编程范式,Apple 于 2015 年 WWDC 提出,在 Swift 的标准库中,能见到大量 POP 的影子。

同时,Swift 是一门面向对象的编程语言(Object Oriented Programming,简称 OOP),在 Swift 开发中,OOP 和 POP 是相辅相成的,任何一方并不能取代另一方ios15。POP 能Apple弥补 OOP 一些设计上的不足。

一、POP 和 OOP

OOP 的三大特性:封装、继承、多态。

举一个继承的使用场合的例字符间距子:当多个类(比如 A、B、C 类)具有很多共性时,可以将这些共性抽取到一个父类https认证中(比如 D 类),最后 A、Bios模拟器、C 类继承 D 类。

iOS-Swift 独孤九剑:十三、面向协议编程

但是有些问题使用 OOP 并不能很好的解决,举个例子:将 BVC 和 DVC 的公共方法 run 抽取出来。

class BVC: UIViewController {
    func run() {
        print("run")
    }
}
class DVC: UITableViewController {
    func run() {
        print("run")
    }
}

我们来看一下 BVC 和 DVC 的关系。https认证

iOS-Swift 独孤九剑:十三、面向协议编程

基于 OOP 我们可能想到这么一些解决方案:

  • 将 run 方法放到另一个对象 A 中,然后 BVC、DVC 拥有对象A属性,但是这样子多了一些额外的依赖关系。

  • 将 run 方法增加到 UIViewController 分类中,但是 UIViewios16要来了Coios系统ntroller 会越来越臃肿,而且有可能会影响它的ios是什么意思其他所有子类。

很显然,采用 OOP 的方式去解决多多少少都会有些缺点,并不是那么完美。我们来看一下 POP 是怎么去解决的。

protocol Runnable {
    func run()
}
extension Runnable {
    func run() {
        print("run")
    }
}
class BVC: UIViewController, Runnable {
}
class DVC: UITableViewController, Runnable {
}

通过协议将 run 方法抽取出来,然后利用协议的 extension 去实现 run 方法。当某个控制器需要用apple官网到 run 方法的时候,直接遵守相关apple id密码重置的协议就拥有 run 方法了,这样就ios模拟器能够弥补 OOP 实现的一些不足。

iOS-Swift 独孤九剑:十三、面向协议编程

如果我们遇到更复杂ios越狱的继承关系,用 POP 去实现会更加明显的感觉到 POP 实现的好处。

使用 POP 的注意点

  • 优先考虑创建协议,而不是父类(基类)。
  • 优先考虑值类型(struct、enum),而不是引用类型(class)apple store
  • 巧用协议的扩展功能。
  • 不要为了面向协议而使用协议。

二、利用协议实现前缀效果

1. OC 给方法添加前缀

在 OC 中给某个app id注册类扩展一些方法时我们通常会给这个类的分类中添加扩展的方法,而且为了防止与源码1688原生的方法或者第三方库的一些方法产生冲突,通常会在方法的前面加上自己的前缀。

@interface NSObject (SHRun)
- (void)sh_run;
@end
@implementation NSObject (SHRun)
- (void)sh_run {
    NSLog(@"run");
}
@end
NSObject *objc = [NSObject new];
[objc sh_run];

前缀的书写格式通常为:<前缀>_<方法>。但是在 Swift 中采用这种方式的话不太好,这么去写感觉和 OC 并没有什么区别,不能体现出 Swift 的特性https安全问题。那怎么给 Swift 添加前缀呢?

2. Swift 给方法添加前缀

假设我要给 String 添加一个 numbehttps协议rCount 方法用来获取字符串中包含数字的个数,numberCount 的实现字符串是什么意思如下:

func numberCount(string: String) -> Int {
    var count = 0
    for c in string where ("0"..."9").contains(c) {
        count += 1
    }
    return count
}

这个方法的缺点很明显,他是一个全局的方法,并且需要传一个 String 类型。既然他是 String 独有的方法,我们可以把 numberCount 抽取到 String 的 extens字符间距在哪里设置ion 中。

extension String {
func numberCount() -> Int {
    var count = 0
    for c in self where ("0"..."9").contains(c) {
        count += 1
    }
    return count
    }
}

这么写还是有问题,既然是给系统的类扩展方法,很有可能扩展的方法名会ios15有冲突。解决冲突的方法有字符间距两种,像 OC 一样,给 String 加一个前缀,前源码资本缀的书写格apple id密码重置式和 OC的一样:<前缀>_<方法>

还有一种方法就是给 String 添加一个前缀类型,以 <前缀>.<方法> 的形式去调用方法。既然是以 <前缀iOS>.<方法> 的形式去调用,很显然,这个前缀是一个属性。

struct SHBase {
    var string: String
    func numberCount() -> Int {
        var count = 0
        for c in string where ("0"..."9").contains(c) {
            count += 1
        }
        return count
    }
}
extension String {
    var sh: SHBase { SH(string: self) }
}
let count = "123abc456".sh.numberCount()

这样子就可以实现以 <前缀>.<方法> 的形式去ios是什么意思调用方法源码交易平台了。如果其他的类型也想以:<前缀>.<方法>https协议 这种方式去调用方法,难道要给 SH 类型添加一个要扩展的类型的属性吗?这样子会产生很多问题,此时可以用泛型来解决这个问题。

struct SHBase<Base> {
    var base: Base
    init(_ base: Base) {
        self.base = base
    }
}
extension String {
    var sh: SHBase<String> { SHBase(self) }
}
"123abc456".sh

此时 String 类型的实例就可以以 .sh源码编辑器下载 的形式调用了,这个时候再给 Person 添加一个 sh 的前缀属性也是一样的,代码如下:

class Person {}
extension Person {
    var sh: SHBase<Person> { SHBase(self) }
}
let person = Person()
person.sh

此时,怎么去扩展东西呢。注意!我们最终以 <前缀>.<方法> 方式调用的方法是谁的,是前缀类型的方法吧,所以最终是给 SHBase 扩展方法。比如字符是什么给 String 类型扩展一个 num字符是什么berCount 方法。

首先,一个前缀类型属性是要有的,所以给 String 的 extension 添加一个前缀类型属性:

extension String {
    var sh: SHBase<String> { SHBase(self) }
}

然后,通过 extension 给前缀类型添加一个 numberCount 方法,在 extension 中判断前缀类型的泛型是否是 String。

extension SHBase where Base == String {
    func numberCount() -> Int {
        var count = 0
        for c in base where ("0"..."9").contains(c) {
            count += 1
        }
        return count
    }
}
let count = "123abc456".sh.numberCount()

此时我们就可以通过 <前缀>.<方法> 的方式调用方法了,而且这种方式不会调用出其他类型的方法,比如 Person 的 run 方法。如果给 Person 也添加一个扩展的方法,我们可以照猫画虎,代码如下:

extension Person {
    var sh: SHBase<Person> { SHBase(self) }
}
extension SHBase where Base: Person {
    func run() {
        print("run")
    }
}
let person = Person()
person.sh.run()

到这里,基本可以实现以 <前缀>.<方法> 的方式调用方法了,就三个步骤字符间距加宽2磅

  • 声明一个泛型类型前app id注册缀类型。

  • 给需要添加扩展方法的类型以 extensiapple watchon 的方式添加前缀类型属性。

  • 最后给app id注册前缀类型扩展方apple id密码重置法,apple id在前缀类型的 extension 中判断泛型是否是需要扩展的类型并实现扩展的内容。

但此时还是不够完善,因为每次想给某个类型添加前缀的时候都要去 extension 中添加前缀类型属性。此时我们可Apple以通过协议来解决这个问题。 代码如下:

protocol SHCompatible {}
extension SHCompatible {
    var sh: SHBase<Self> { SHBase(self) }
}

这里也是利用了协议的两个特性:一个是协议的 extension 可以添加计算属性和方法;一个ios系统是协议的 Self 可以还原出真实类型。此时只需要给需要添加扩展方法的类型遵守 SHCompatib源码编程器le 协议就拥有 sh 的前缀类型属性了。

extension String: SHCompatible {}
extension Person: SHCompatible {}
let count = "123abc456".sh.numberCount()
let person = Person()
person.sh.run()

这样就不需要给每个需要添加前缀类型的类型添加前缀类型属性了,只需要遵守 SHCompatible 就能拥有前缀类型属性。上面添加的 sh 属性是实例的计算属性,所以只能通过 sh 调用实例方法。此时想调用类方法也很简单,给 SHCompios是苹果还是安卓atible 协议添加一个 static 的 sh。源码中的图片

代码如下:

extension SHCompatible {
    var sh: SHBase<Self> { SHBase(self) }
    static var sh: SHBase<Self>.Type { SHBase<Self>.self }
}
extension SHBase where Base: Person {
    static func run() {
        print("run")
    }
}
Person.sh.run()

还有最后一个细节,遵守 SHCompatible 协议的类型有可能是一个值类型,当在 extension 实现的方法中需要对值类型的内存进行操作的时候,需要添加 mutating,此时我们定义的 sh 属性就不能调用值类型的异变方法。此时我们把 sh 属性修改成可读可写的属性就行字符间距了。

代码如下:

extension SHCompatible {
    var sh: SHBase<Self> {
        get { SHBase(self) }
        set {}
    }
    static var sh: SHBase<Self>.Type {
        get{ SHBase<Self>.self }
        set {}
    }
}
struct Person {
    var age = 10
}
extension Person: SHCompatible { }
extension SHBase where Base == Person {
    mutating func setAge() {
        base.age = base.age + 8
    }
}
var person = Person()
person.sh.setAge()

3. 总结

最后我们来做一个总结,在 Swift 中给类型添加前缀字符间距怎么加宽调用的方式分四个字符步骤:

  • 声明一个前缀类型,这个前缀类型通常是一个 struct。
  • 声明前缀属性的协议,在协议中实现实例的前缀属性和静态类型的前缀属性。
  • 需要apple pay用到前缀调用自定义扩展内容的类型遵守前缀属性的协议。
  • 通过前缀类型的 extension 来实现类型的扩展内容。

完整的ios14.4.1更新了什么例子如下:

// 前缀类型
struct SHBase<Base> {
    var base: Base
    init(_ base: Base) {
        self.base = base
    }
}
// 拥有前缀属性的协议
protocol SHCompatible {}
extension SHCompatible {
    var sh: SHBase<Self> {
        get { SHBase(self) }
        set {}
    }
    static var sh: SHBase<Self>.Type {
        get{ SHBase<Self>.self }
        set {}
    }
}
// 遵守拥有前缀属性的协议的类型
extension String: SHCompatible {}
// 通过前缀类型 extension 实现 String 的扩展内容
extension SHBase where Base == String {
    func numberCount() -> Int {
        var count = 0
        for c in base where ("0"..."9").contains(c) {
            count += 1
        }
        return count
    }
}
// 调用
let count = "123abc456".sh.numberCount()
print(count) // 6

三、Moya

在平时的开发字符间距怎么加宽中我们都会和网络打交道,而 iOS 中绝大多数都是用的第三方库:AFNetworking 或者 Alamofire。这字符间距怎么加宽两者都是对官方的 URLSession 进行封装,避免开发中使用官方那繁琐的 API。

但是在使用 AFNe源码编程器tworking 或者 Alamofire 久了就会发现,App 中到处散字符串是什么意思落着 Aapple payFNetwork字符间距怎么加宽ing 或者 Alamofire 相关的代码,这个时候就导致不便与统一管理。并且会发现,很多代码都是字符是什么重复的,这个源码之家时候我们就会新建一字符个 Network Manager 来管理网络请求相关的代码。

在和网络打交道的时候,我们只要通过封装好的 Network Manager 打交道就可以了,Network Manager 的目的是隔离网络请求的第三方库,当需要替换第三方HTTPS网络请求库的时候,只需要在 Network Manager 的内部进行替换,这样子就对我们上层的业务ios15.4正式版逻辑毫无影响。

如果封装的 Network Manager 不是那么理想的话,可能会导致直接越过 Network Manaapp storeger,进而与底层的网络请求库打交道,我们来看 Moya 提供的一张图:

iOS-Swift 独孤九剑:十三、面向协议编程

所以,其实 Moya 的作用其实就是我们提到的 Neios15twork Manager,它是对是对网络业务逻辑的抽象,我字符是什么们只需要遵循相关协议,就可以发起网络请求,而不用关系底层细节。

Moya文档地址:中文文档;英文文档。

1. Moya 的基本使用

Moya 既然是一个网络请求的中间层,那它的使用和我们平时自己写的 Network Manager 还是有区别的,Moapple id密码重置ya 更加的 Swift 化,并且 Moya 是采用面向协议编程源码交易平台的范式去构建的。

Moya 有一个协议:TargetType;这个协议定义的都是平时需要的基础网络请求数据,而且它的定义也比简单。来看一下 TargetType 长什么样。

iOS-Swift 独孤九剑:十三、面向协议编程

可以看到,Targapple tvetType 中定义的有 baseU字符间距加宽2磅RL、path、metho字符间距加宽2磅d、apple tvheaders 等,在使用的时候就源码资本可以为我们的 API 新建一个文件:MyService.swift,文件内定义一个枚举:MyService。

enum MyService {
    case zen
    case showUser(id: Int)
    case createUser(firstName: String, lastName: String)
    case updateUser(id: Int, firstName: String, lastName: String)
    case showAccounts
}

这个源码编辑器下载枚举中定义都源码精灵永久兑换码是一些 API 入口字符间距,接下来只需要这个枚举遵守 TargetType 协议,就可以填充这些 API 的 baseURL 和 path。

// MARK: - TargetType Protocol Implementation
extension MyService: TargetType {
    var baseURL: URL { return URL(string: "https://api.myservice.com")! }
        var path: String {
        switch self {
        case .zen:
            return "/zen"
        case .showUser(let id), .updateUser(let id, _, _):
            return "/users/(id)"
        case .createUser(_, _):
            return "/users"
        case .showAccounts:
            return "/accounts"
        }
    }
    var method: Moya.Method {
        switch self {
        case .zen, .showUser, .showAccounts:
            return .get
        case .createUser, .updateUser:
            return .post
        }
    }
    var task: Task {
        switch self {
        case .zen, .showUser, .showAccounts: // Send no parameters
            return .requestPlain
        case let .updateUser(_, firstName, lastName):  // Always sends parameters in URL, regardless of which HTTP method is used
            return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: URLEncoding.queryString)
        case let .createUser(firstName, lastName): // Always send parameters as JSON in request body
            return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: JSONEncoding.default)
        }
    }
    var sampleData: Data {
        switch self {
        case .zen:
            return "Half measures are as bad as nothing at all.".utf8Encoded
        case .showUser(let id):
            return "{"id": (id), "first_name": "Harry", "last_name": "Potter"}".utf8Encoded
        case .createUser(let firstName, let lastName):
            return "{"id": 100, "first_name": "(firstName)", "last_name": "(lastName)"}".utf8Encoded
        case .updateUser(let id, let firstName, let lastName):
            return "{"id": (id), "first_name": "(firstName)", "last_name": "(lastName)"}".utf8Encoded
        case .showAccounts:
            // Provided you have a file named accounts.json in your bundle.
            guard let url = Bundle.main.url(forResource: "accounts", withExtension: "json"),
                let data = try? Data(contentsOf: url) else {
                return Data()
            }
                return data
        }
    }
    var headers: [String: String]? {
        return ["Content-type": "application/json"]
    }
}
// MARK: - Helpers
private extension String {
    var urlEscaped: String {
        addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
    }
    var utf8Encoded: Data {
        Data(self.utf8)
    }
}

通过 TargetType 就可以把请求需要的 urlStr源码之家ing、参数、请求的方式定义好了。这个时候你会发现,我们所有请求需要做的准备已经通过 Tar字符是什么getType 写好了,下一步就是发起网络请求。

在外部使用 Moya 进行网源码中的图片络请求的时候,只需要通过 MoyaProvider 对象来发送请求即可:

let provider = MoyaProvider<MyService>()
provider.request(.createUser(firstName: "James", lastName: "Potter")) { result in
    // do something with the result (read on for more details)
}
provider.request(.updateUser(id: 123, firstName: "Harry", lastName: "Potter")) { result in
    // do something with the result (read on for more details)
}

我们还可以自定字符间距怎么加宽义 Endapple storepoint,字符间距在哪里设置做一些处理:

let endpointClosure = { (target: MyService) -> Endpoint in
    return Endpoint(url: URL(target: target).absoluteString, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: target.method, task: target.task)
}

Endios15point 对象在下面会讲,这里只需要有个印象就可以了,最后一个点,当我们发起网络请求之后,如何拿到网络请求中的数据呢。字符串怎么输入

provider.request(.zen) { result in
    // do something with `result`
}

request 方法被传递了一个 MySer源码编辑器vice 值 (.zen), 它包含了用来创建 Endpoint的所有必须的信息,Endpoint源码时代 实例对象被用来创建一个 URLRequest (繁重的工作已通过 Alamofire 完成的), 并且 request 被发送 (也是被 – Alamofiapp storere). 一旦 Alamofire 得到了一个响应 (或者没有得到响应), Moya 将用 enumResios14.4.1更新了什么ult 类型包裹成功或者失败. result 要么是 .success(Moya.Respon源码精灵永久兑换码se) 要么字符常量是 .failure(MoyaError)。

所以,我们可以从 Moyapple paya.Response 中拿到我们需要的数据:

provider.request(.zen) { result in
    switch result {
    case let .success(moyaResponse):
        let data = moyaResponse.data // Data, your JSON response is probably in here!
        let statusCode = moyaResponse.statusCode // Int - 200, 401, 500, etc
        // do something in your app
    case let .failure(error):
        // TODO: handle the error == best. comment. ever.
    }
}

2. MoHTTPSya 的构建

在看了 Moya 的源码https和http的区别后总结出来的模块大致可以分成五个模块:

iOS-Swift 独孤九剑:十三、面向协议编程

而 Moya 主要的数据处理流程可以用下面这张图来表示:

iOS-Swift 独孤九剑:十三、面向协议编程

MoyaProvider

M字符间距在哪里设置oya 首先通过 Targios模拟器etType 来定义网络请求的基本配置信息,之后由 MoyaProvidhttps协议er 对象来发起真正的网络请求。MoyaProvider 遵守一个协议:MoyaProviderType;MoyaProviderType 的定义如下:

iOS-Swift 独孤九剑:十三、面向协议编程

可以看到,MoyaProviderType 其实就是定义了一个 request 的请求方法,通过关联类型来要求第一个参数必须遵守 TargetType 协议类型字符间距怎么加宽的泛型参数。来看一下 MoyaProvider 是如何实现协议的请求方法的:

iOS-Swift 独孤九剑:十三、面向协议编程

可以看到其内部调用了一个 requestNo源码编程器rmal 方法,在 requestNormal 的内部,https协议会先把遵守 TargetType 协议的类型字符生成一个 Endpoint 的类型。

iOS-Swift 独孤九剑:十三、面向协议编程

Endpoint 是一个 class,其内部包含的是 TargetType 中的部分内容,例如 url,method 等。

iOS-Swift 独孤九剑:十三、面向协议编程

在 requestNormal 方法的内部,最终会调用 requestClosureapp id注册,这个 requestClosure 其实是一个闭包表达式,调用 requestClosuios15.4正式版re 的代码如下:

requestClosure(endpoint, performNetworking)

可以看到,requios是什么意思estClosure 接收了 endpoint 和一个 performNetworking,这个 performNetworki源码编辑器下载ng 很显然就是网络请求操作。在最后 requestNohttps安全问题rmal 会返回一个 Cancellios系统ableWrapper 的类型,Cahttps和http的区别ncellableWra字符是什么pper 用于管理是否取消当前的网络请求任务。

这个 requestClosure 在内部做了什么呢,来看一下这个闭包表达式是在哪里赋值的:

iOS-Swift 独孤九剑:十三、面向协议编程

在 MoyaProvider 进行初始源码编辑器化的时候,req源码编辑器uestClosure 默认就有一个初始值,并且 endpointClosure 也有一个默认的初始值(endpointClosure 在下面会讲)。

可以看到 re字符间距questClosure 本质上是 MoyaProvider.defaultRequestMapios15.4值得更新吗ping,我们来看看这个函数字符间距怎么加宽在内部都做了些什么:

iOS-Swift 独孤九剑:十三、面向协议编程

可以看到,defaultRequestMapping 方法就只做一件事,通过 Endpoint 来生成 URLRequest。我们再来回顾 requestNormal 方法中是如源码何生成 Endpoint 的,在 requestNormal 方法中通过调apple watch用 endpoint 方法来生成 Endpoint,其内部的实现也非常简单,就是调用了刚刚提到的闭包表达式:endpointClosure。

iOS-Swift 独孤九剑:十三、面向协议编程

endpointClosureApple 本质上是 MoyaProvid源码时代er.defau源码编辑器下载ltEndpointMapping,这个方法就是用来生成 Endpoint 的app store,我们来看一下:

iOS-Swift 独孤九剑:十三、面向协议编程

前面也提到了,Endpoint 是用来生成 URLRequest,那么 Endp源码精灵永久兑换码oint 是如何生成 URLRequest 的呢。其实也很简单,Endpoint 会根据当前的 task 生成不同类型的 URLRequest。

iOS-Swift 独孤九剑:十三、面向协议编程

生成 URLRequest 之后就开始发起网络请求了,在 re源码之家questNormios15.4值得更新吗al 的闭包:performNetworking 中调用 performRequest 方法,这个方法就是 Moya 发起网络请求的入口。

iOS-Swift 独孤九剑:十三、面向协议编程

在 performRequest 中会根据 Endpoint 的 task 来处理不同的请求,但其实这些方法的内部最终调用的都是 sendAlamofireRequest 方法。sendAlamofireRequest 通过 Moya 对 Al字符间距amofire 封装好的方法进行发起网络请求。

iOS-Swift 独孤九剑:十三、面向协议编程

Moya+Alamofire

Moya 对 Alamofire 的封装非常简单,通过 Reios15questable 协议对 Alamofire 的 Request 进行扩展,Requestable 中只声明了一个 response 方法,这个方法返回的是一个 Se字符串逆序输出lf 类型。通过 Sel源码编辑器f 可以拿到 Alamofireios15.4值得更新吗 中 Request 的真实类型,进而调用 resume 方法来开始当前的请求。

iOS-Swift 独孤九剑:十三、面向协议编程

我们最后看一下 DataReq字符串怎么输入uest 和 Dow字符串逆序输出nloadRequest 是什么:

iOS-Swift 独孤九剑:十三、面向协议编程

Moya 主要的数据处理ios15流程到这里就结束了,在发起网络ios越狱请求之后的回调处理都是在 sendAlamofireRequest 方法中。