面向协议编程_ios 面向协议编程 - CSDN
精华内容
参与话题
  • 喵神完整导演未剪辑带笔记版本
  • 浅析面向协议编程

    2019-02-28 15:24:03
    或许你经常听到诸如面向对象编程、面向过程编程、面向协议编程、函数式编程这些词,心中也不免疑惑,这些词都是些啥? 编程范式 on wiki 相比于本文要介绍的面向协议编程,面向对象编程的名声似乎更响。面向对象由于...

    从编程范式讲起

    或许你经常听到诸如面向对象编程、面向过程编程、面向协议编程、函数式编程这些词,心中也不免疑惑,这些词都是些啥?

    编程范式 on wiki

    相比于本文要介绍的面向协议编程,面向对象编程的名声似乎更响。面向对象由于C++java的流行,成为近二十年来最为流行的**编程范式。其他编程范式还有比面向对象更早的面向过程编程**、指令编程,以及新兴的以Haskell为代表的**函数式编程和苹果提出的面向协议*编程。

    编程范式是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照方法学)。编程范型提供了(同时决定了)程序员对程序执行的看法。 ——wiki

    我理解的编程范式

    说说我的理解,我认为编程范式**反应了某种编程语言的设计者希望程序员在使用他设计的语言时,用什么样的方式去思考问题。**因此不同的编程范式各有自身的长处,也难免有不足之处,在应对各种问题的时候,某种编程范式可能会更适合一些,选择得当,程序员的工作量会减少很多。

    一种语言,可能支持多种编程范式,例如Swift,既支持面向协议编程,又支持面向对象编程、面向协议编程和函数式编程。只是在解决不同任务时,某些范式更合适。

    Swift & 面向对象编程

    举个例子,由于历史原因,在开发Cocoa和cocoa touch程序时,还是广泛使用了UIkit、Foundation框架和控制器视图(ViewController),他们使用的是OC时代的面向对象编程的方式,从继承的角度去考虑问题的。

    UIkit框架类组织架构图

    Swift & 面向过程编程

    在解决动画渲染数据可视化等问题时,还是以古老的面向过程式的思维方式进行编程。因为在画图时,先绘制的东西会被后绘制的东西盖住,形成**层(Layer)**的概念。如果不能保证代码是同步一条条执行,那很可能每次渲染出来的图形是不同的。

    Swift & 函数式编程

    函数式编程本身是一个很大的课题,精髓是避免使用程序状态和可变对象,从而降低程序复杂度。函数式编程强调执行的结果,而非执行的过程。我们先构建一系列简单却具有一定功能的小函数,然后再将这些函数进行组装以实现完整的逻辑和复杂的运算,这是函数式编程的基本思想。

    Swift在函数式编程方面表现虽不如Haskell来的纯粹,但是作为一个比Haskell流行的多的语言,和很多已有的函数式编程语言,Swift在语法上更加优雅灵活,语言本身也遵循了函数式的设计模式,是大部分程序员接触函数式编程的第一门语言。以后有机会单独介绍一下函数式编程,有兴趣的读者也可以先看chase Zhang的这篇blog

    面向协议编程出现的历史

    回到今天的主角,面向协议编程。面向协议编程是由面向对象编程演变来的,协议在诸如c++和java等面向对象的语言中有一个别名,接口。(别骂街、别关网页,好戏在后头)

    面向对象编程的好处

    面向对象编程这些年能够风光无限,主要是由于它有的很多优点,例如:

    1. 数据封装
    2. 访问控制
    3. 类型抽象为类
    4. 继承关系,更符合人类思维
    5. 代码以逻辑关系组织到一起,方便阅读
    6. 由于继承、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低
    7. 在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量

    这么多优点,不可能一下全抛弃,所以也注定了面向协议编程不是一种革命性的编程范式,而是对面向对象编程的改良和演变。

    Who is Crusty at Apple

    长久以来,大家似乎默认了面向对象编程的好处都是由class带来的,但在苹果公司内部,有个叫Crusty的老兄think different了,他认为这一切是抽象类型带来的,而不是class带来的。我们知道,在面向对象编程中,接口和类都是对数据类型的抽取,类只是抽象类型众多实现的手段之一。在很多编程语言中,Struct和枚举同样可以做到对数据类型的抽取。

    It's Type, not Classes. ————Crusty

    Crusty认为,较好的抽象类型应该:

    1. 更多地支持值类型,同时也支持引用类型
    2. 更多地支持静态类型关联(编译期),同时也支持动态派发(runtime)
    3. 结构不庞大不复杂
    4. 模型可扩展
    5. 不给模型强制添加数据
    6. 不给模型增加初始化任务的负担
    7. 清楚哪些方法该实现哪些方法不需实现

    经过改良的接口,达到了上面的要求,并改名为协议。至此,苹果决定将面向对象中的继承父类发展为服从协议,面向协议编程出现了。

    面向协议编程的好处

    前面提到了,经过Apple改良的接口达到了上节提出的较好抽象类型的目标。Swift的面向协议编程相比于OC的面向对象编程的好处主要体现在两点:1.动态派发的安全性、2.横切关注点

    动态派发的安全性

    OC有强大的Runtime,在OC中,message与方法是在执行阶段绑定的,而不是编译阶段。简单的说 [a someFunc] 这样一个调用,在编译阶段,编译器并不知道someFunc要执行哪段代码。这个时候[a someFunc]会被转换为 objc_msgSend(a, "someFunc"),字面的意思也很容易理解,就是给a这个instance,发“someFunc”这个消息,以selector的形式。在运行阶段,执行到上述的objc_msgSend这个函数时。函数内部会到a对应的内存地址,寻找someFunc这个方法的地址,并执行。如果找不到,就会抛一个“unknown selector sent to instance”的异常。(比如.h中声明了方法,但.m中没有实现,就可以重现这个错误)

    下面举的例子来自于喵神在MDCC 16上的演讲《面向协议编程与 Cocoa 的邂逅》中的ppt:

    
    ViewController *v1 = ...
    [v1 myMethod];
    AnotherViewController *v2 = ...
    [v2 myMethod];
    NSObject *v3 = [NSObject new] // v3 ���� `myMethod`
    NSArray *array = @[v1, v2, v3];
    for (id obj in array) {
        [obj myMethod];
    }
    // Runtime error:
    // unrecognized selector sent to instance blabla
    
    复制代码

    上面的代码是可以编译过的,但是在运行时程序会崩溃。有了协议(protocol),可以申明数组的每个对象都是遵从某个协议的,如果塞进去了不遵从该协议的对象,就会报错,通不过编译。

    
    protocol Greetable {
        var name: String { get }
        func greet()
    }
    struct Cat: Greetable {
        let name: String
        func greet() {
            print("meow~ \(name)")
    } }
    
    let array: [Greetable] = [
            Person(name: "Wei Wang"),
            Cat(name: "onevcat")]
    for obj in array {
        obj.greet()
    }
    
    struct Bug: Greetable {
        let name: String
    }
    // Compiler Error:
    // 'Bug' does not conform to protocol 'Greetable'
    // protocol requires function 'greet()'
    
    复制代码

    横切关注点(Cross-Cutting Concerns)

    由于大部分面向对象的编程语言都是单继承的,导致了某些功能是不同类之间都需要的,但是由于改类已经继承了其他类,或者无法将不同类之间抽取出更多共性(或者说对于某一个小功能点来这样做代价太大),成为他们的父类,这样不得不在每个类里面重复一遍代码,使得代码很冗长。这样的小功能就可以称为横切关注点。

    还是直接拿喵神的例子,假设我们有一个 ViewController,它继承自UIViewController,我们向其中添加一个 myMethod,如果这时候我们又有一个继承自 UITableViewController 的 AnotherViewController,我们也想向其中添加同样的 myMethod,这时,我们迎来了 OOP 的一大困境,那就是我们很难在不同继承关系的类里共用代码。这里的问题用“行话”来说叫做“横切关注点” (Cross-Cutting Concerns)。我们的关注点 myMethod 位于两条继承链 (UIViewController -> ViewCotroller 和 UIViewController -> UITableViewController -> AnotherViewController) 的横切面上。面向对象是一种不错的抽象方式,但是肯定不是最好的方式。它无法描述两个不同事物具有某个相同特性这一点。在这里,特性的组合要比继承更贴切事物的本质。

    在swift中很容易抽取出协议,并用extension关键字提供协议的默认实现,从而避免的代码的重复。

    参考

    1.《面向协议编程与 Cocoa 的邂逅》 2.wiki:编程范式 3.A Brief Intro to Functional Programming 4.Objective-C 的消息机制如何理解

    展开全文
  • 面向协议编程

    2019-09-17 11:24:24
    面向协议编程 在使用 Swift 时,可以为结构体和枚举类型声明方法,其使用起来更加方便,对于一些简单模型,或者对象的组合模型,使用结构体更为简便。 对于面向对象编程(OOP,Object Oriented Programming)而言,...

    面向协议编程

    在使用 Swift 时,可以为结构体和枚举类型声明方法,其使用起来更加方便,对于一些简单模型,或者对象的组合模型,使用结构体更为简便。

    对于面向对象编程(OOP,Object Oriented Programming)而言,类能够更好的描述模型,并且可以将抽象出的通用属定义在父类中。

    但是,不管是 Objective-C 还是 Swift 语言,都不支持多继承,但是他们都可以遵循多个协议。实际编程时,可以将不同的特性分类,定义在不同的协议中,然后通过遵循协议的方式来实现一类特性。

    在 Objective-C 中,值类型既不继承其他类型,也不能实现协议。如果要更多的使用值类型,那么抽象出通用的属性给不同值类型继承,就十分必要。而 Swift 做得更多,所有的类型都可以遵循多个协议,而且协议本身可以实现默认的方法,如此,针对一类操作定义一个协议,那么只要遵循该协议,就可以获得相应操作。

    如同一个孩子的发质继承自父亲,而眼睛更像母亲,那么如果使用面向对象的方式进行描述,无法支持多继承的情况下,只能冗余的将特性既声明在父类中又声明在子类中。而如果将特性声明在协议中,父类和子类遵循相同的协议就表示拥有同一个特质,而子类再遵循一个描述母亲眼睛特性的协议即可。

    所以,面向协议编程(POP,Protocol Oriented Programming)是通过不同协议的继承和组合来描述事物。可以说,面向对象更多的是对一类事物通用特性的抽象,而面向协议更多的是对不同事物通用特性的抽象。前者更注重整体特性的抽象,后者是对特性更细粒度的抽象。通过不同特性的组合来描述一类事物,比通过层层继承来描述一类事物更加简化,更加具体,代码冗余度也会下降。

    例如,在应用架构的设计时,也可以使用面向协议编程的思想,我们可以将以前写入基类中的一些属性或方法,定义在协议中,通过遵循协议来使相应的类型获得需要的属性或方法。

    protocol HXJRootProtocol: NSObject {
        
        var isSafeContentViewEnable: Bool { get set }
        
        var safeContentView: UIView! { get }
    }
    
    extension UIViewController: HXJRootProtocol {
        
        private static let safeContentViewTag: Int = 1000
        
        var safeContentView: UIView! {
            assert(isSafeContentViewEnable, "需要先设置 isSafeContentViewEnable 为 true 才可以使用 safeContentView")
            if let view = view.viewWithTag(UIViewController.safeContentViewTag) {
                return view
            }
            return nil
        }
        
        
        var isSafeContentViewEnable: Bool {
            set {
                if newValue {
                    if nil != view.viewWithTag(UIViewController.safeContentViewTag) { return }
                    addSafeContentView()
                }else {
                    if let view = view.viewWithTag(UIViewController.safeContentViewTag) {
                        view.removeFromSuperview()
                    }
                }
            }
            
            get {
                return view.viewWithTag(UIViewController.safeContentViewTag) != nil
            }
        }
        
        private func addSafeContentView() {
            
            let safeContentView = UIView.init()
            view.addSubview(safeContentView)
            
            safeContentView.backgroundColor = .clear
            safeContentView.tag = UIViewController.safeContentViewTag
            safeContentView.translatesAutoresizingMaskIntoConstraints = false
            
            safeContentView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
            safeContentView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
            
            if #available(iOS 11.0, *) {
                safeContentView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
                safeContentView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
            } else {
                safeContentView.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor).isActive = true
                safeContentView.bottomAnchor.constraint(equalTo: self.bottomLayoutGuide.topAnchor).isActive = true
            }
        }
            
    }
    

    在上面的代码中,通过使 UIViewController 遵循 HXJRootProtocol 协议,来添加一个安全区域的视图。

    虽然无法为一个类型扩展存储属性,但是如果有必要,可以使用运行时的方法来实现该需求。

    展开全文
  • Swift面向协议编程(附代码)

    千次阅读 2017-02-16 14:20:47
    什么是swift协议?Protocol Swift标准库中有50多个复杂不一的协议,几乎所有的实际类型都是妈祖若干协议的。protocol是Swift语言的底座,语言的其他部分正是在这个底座上组织和建立起来的。这和我们熟知的面向对象...

    什么是swift协议?

    Protocol
    Swift标准库中有50多个复杂不一的协议,几乎所有的实际类型都是妈祖若干协议的。protocol是Swift语言的底座,语言的其他部分正是在这个底座上组织和建立起来的。这和我们熟知的面向对象的构建方式很不一样。

    一个最简单但是有实际用处的Swift协议定义如下:

    protocol Greetable {
        var name: String{get}
        func greet()
    }

    这几行代码定义了一个名为Greetable的协议,其中有一个name属性的定义,以及一个greet方法的定义。

    所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是“关于实现的约定”。

    面向对象

    在深入Swift协议的概念之前,我想先重新让大家回国一下面向对象。相信我们不论在教科书或者是博客等各种地方对这个名词都十分熟悉了。那么又一个很有意思,但是其实并不是每个程序员都想过的问题,面向对象的核心思想究竟是什么?

    class Animal {
        var leg:Int {return 2}
        func eat() {
            print("eat food")
        }
        func run() {
        print("run with \(leg)legs")    
        }
    }
    
    class Tiger:Animal {
        override var leg: Int {return 4}
        override func eat() {
        print("eat meat")
        }
    }
    
    let tiger = Tiger()
    tiger.eat()//eat meat
    tiger.run()//run with 4 legs

    父类Animal 定义了动物的leg,以及动物的eat和run方法,并未她们提供了实现。子类的Tiger根据自身情况充血了leg和eat,而对于run,父类的实现已经满足要求,因此不必重写。

    我们看到Tiger和Animal共享了一部分代码,这部分代码还被封装到了父类中,而除了Tiger的其他的字类也能够使用Animal的这些代码。这其实就是OOP的核心思想-使用封装和集成,将一些列相关的内容放到一起。我们的前辈们为了能够对真实的世界的对象进行建模,发展出了面向对象编程的概念,但是这套理念有一些缺陷。虽然我们努力用这套抽象和集成的方法进行建模,但是实际的食物往往是一系列特质的组合,而不单单是以一脉相承并逐渐扩展的方式构建的。所以最近大家越来越发现面向对象很多时候其实不能很好的对食物进行抽象,我们可能需要寻找另一个更好的方式。

    面向对象编程的困境

    横切关注点

    我们再来看一个例子。这次让我们远离动物界,回到Cocoa,假设我们又一个VIewController,它集成自UIViewController,我们向其中添加一个myMethod:

    class ViewControllerUIViewControlller{
        func myMethod() {
        }
    }

    如果这个时候我们又有一个继承自UITableviewController的AntherViewController,我们也想向其中添加同样的myMethod:

    class AnotherViewControllerUITableViewController{
        func myMethod(){
        }
    }

    这是我们迎来了OOP的第一大困境,那就是我们很难再不同集成关系的类里共用代码。这里的问题用“行话”来说叫做“横切关注点(cross-cutting concerns)”。我们的关注点myMethod位于两条继承链的横切面上。面向对象是一种不错的抽象方式,但是肯定不是最好的方式。它无法描述两个不同食物具有某个相同特性这一点。在这里,特性的组合要比集成更贴切事物的本质。

    想要解决这个问题,我们有几个方案:

    • Copy&Paste
      这是一个比较糟糕的解决方案,但是演现场还是有不少朋友选择了这个方案,特别是在工期很紧,无暇优化的情况下。这诚然可以理解,但是这也是坏代码的开头。我们尽量避免这种做法。
    • 引入BaseViewControlller
      在一个集成自UIViewController的BaseViewControlller上添加需要共享的代码,或者干脆在UIViewController上添加extension。看起来这是一个稍微靠谱的做法,但是如果不断这么做,会让所谓的Base很快变成垃圾堆。指责不明确,任何东西都扔进Base,你完全不知道那些类走了Base,而这个超级类对代码的影响也会不可估量。
    • 依赖注入
      通过外界传入一个带有myMethod的对象,用新的类型来提供这个功能。这是一个稍好的方式,但是引入额外的依赖关系,坑能也是我门不太愿意看到的。
    • 多继承
      当然,Swift是不支持多继承的,不过如果有多集成的话,我们确实可以从多个父类进行集成,并将myMethod添加到合适的地方。有一些语言选择了支持多集成,但是它会带来OOP中另一个著名的问题:菱形缺陷

    菱形缺陷

    上面的例子中,如果我们有多继承,纳闷ViewController和AnotherViewController的关系可能会是这样的:
    这里写图片描述
    在上面这种拓扑结构中,我们只需要在ViewContorller中实现myMethod,在AnotherVIewController中也就可以继承并使用它了。看起来很完美,我们避免了重复。但是多集成有一个无法回避的问题,就是两个父类都实现了同样的方法时,自乐该怎么办,我们很难确定因该继承哪一个父类的方法。因为多集成的拓扑结构是一个菱形,所以这个问题又被叫做菱形缺陷。像是C++这样的语言选择粗暴的将菱形缺陷的问题交给程序员处理,这无疑非常复杂,并且增加了人为错误的可能性。二绝大多数现代语言对多集成这个特性选择避而远之。

    动态派发安全性

    OC恰如其名,是一门典型的OOP语言,同时它继承了SmallTalk的消息发送机制。这套机制十分灵活,是OC的基础思想,但是有时候相当危险。

    ViewController *v1 = ...
    [v1 myMethod];
    AnotherViewController *v2 = ...
    [v2 myMethod];
    
     NSArray *array = @[v1,v2];
     for (id obj in array){
     [obj myMethod];
    }

    我们如果在ViewController和AnotherViewController中都实现了myMethod的话,这段代码是没有问题的。myMethod将会呗动态发送个array中的v1和v2。但是,要是我们有一个没实现myMethod的类型,会如何?

    NSObject *v3 = [NSObject new];
    NSArray *array = @[v1,v2,v3];
    for (id obj in array){
        [obj myMethod];
    };

    编译依然可以通过,但是显然,程序将在运行时崩溃。OC是不安全的,编译器默认你知道某个方法确实有实现,这是消息发送的灵活性所必需付出的代价。而在app开发看来,用可能的崩溃来换取灵活性,显然这个代价太大了。肃然这不是OOP范式的问题,但是它确实在OC时代给我们带来了切肤之痛。

    三大困境

    我们可以总结一下OOP面临的这几个问题。

    • 动态派发安全性
    • 横切关注点
    • 菱形缺陷

      首先,在OC中动态派发让我们承担了在运行时才发现错误的风险,这很很有可能是发生在上线产品中的错误。其次,横切关注点让我们难以对对象进行完美的缄默,代码的重用也会更加糟糕

    协议扩展和面向协议编程

    使用协议解决OOP困境

    协议并不是什么新东西,也不是Swift的发明。在Java和C#里,它叫做Interface。二Swift中的Protocol将这个概念继承了下来,并发扬光大。让我们回到一开始定义的那个简单协议,并尝试实现这个协议:

    protocol    Greetable{
         var name : String{get}
         func greet()
    }
    struct Person:Greetable{
        let name :String
        func greet(){
        print("你好\(name)")
        }
    }
    Person(name:"zq").greet()

    实现很简单,Person结构体通过实现name和greet来满足Greetable。在调用时,我们就可以使用Greetable中定义的方法了。

    动态派发安全性

    除了Person,其他类型也可以实现Greetable,比如Cat:

    struct Cat: Greetable {
        let name: String
        func greet() {
        pring("meow~\(name)")
        }
    }

    现在,我们就可以将协议作为标准类型,来对方法调用进行动态派发了:

    let array: [Greetable] = [
        Person(name:"zq");
        Cat(name:"zz")]
    for obj in array{
        obj.greet()
    }

    对于没有实现Greetbale的类型,编译器将返回错误,因此不存在消息错误发送的情况

    横切关注点

    使用协议和协议扩展,我们可以很好的共享代码。回到上一节的myMethod方法,我们来看卡如何使用协议来搞定它。首先,我们可以定义一个含有myMethod的协议:

    protocol P {
        func myMethod()
    }   

    注意这个协议没有提供任何实现。我们依然需要在实际类型遵守这个协议的时候为它提供具体的实现:

    extension ViewController: P {
        func myMethod() {
        doWork()
        }
    }
    
    extension AnotherViewController: P {
        func myMethod() {
        doWork()
        }
    }

    这和Copy&Paste也没什么不同啊?没有!听说过协议扩展吗 继续看

    extension P {
        func myMethod() {
            doWork()
        }
    }

    给协议做扩展,有了这个扩展后我们只需要简单的声明ViewController和AnotherViewController遵守P,就可以直接使用myMehtod的实现了:

    extension ViewController: P {}
    extension AnotherViewController: P {}

    不仅如此,除了已经定义过的方法,我们甚至可以在扩展中添加协议里没有定义过的方法。在这些额外的方法中,我门可以依赖协议定义过的方法进行操作。

    • 协议定义
      ·提供实现的入口
      ·遵循协议的类型需要对其进行实现
    • 协议扩展
      ·为入口提供默认实现
      ·根据入口提供额外实现
      这样一来,横切关注的问题也简单安全的得到了解决。

    菱形缺陷

    最后我们看看多继承。多集成中存在的一个重要问题是菱形缺陷,也就是字类无法确定使用哪个负累的方法。在协议的对应方面,这个问题虽然依然存在,但却是可以为宜安全的确定的。

    protocol Nameable {
        var name: String{get}
    }
    protocol Identifiable {
        var name: String{get} 
        var id: Int {get}
    }
    如果有一个类型,需要同时实现两个协议的话,他必须提供一个name属性,来同时满足两个协议的要求
    

    struct Person: Nameable, Identifiable {
    let name :String
    let id: Int
    }

    这样以来菱形缺陷的问题就基本得以解决
    
    #在日常开发中使用协议
    
    
    ##基于Protocol的网络请求
    网络请求层是实践POP的一个理想场所。我们在接下的例子中将从零开始,用最简单的面向协议的方式向偶见一个不那么完美的网络请求和模型层,他肯呢个包含一些不合理的设计和耦合,但是却是起步最容易得到的结果。然后我们逐步捋顺各部分的所属,并用分离职责的方式来进行重构。最后我们会为这个网络请求层进行测试。通过这个例子,我希望能够设计出包括类型安全,解耦合,易于测试和良好的扩展性等诸多优秀特性在内的POP代码。
    
     1. 初步实现
    
    首先我们想要做的事情是从一个API请求一个JSON,然后将它转换成为Swift中可用的实例。作为例子的API非常简单,你可以直接访问https://api.onevcat.com/users/onevcat 来查看返回
    

    {“name”:”onevcat”,”message”:”Welcome to MDCC 16!”}

    我们新建一个项目并使用struct建立一个模型,为什么要使用struct呢?
    

    // 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
    }
    

    }

    User.init(data:)将输入的data数据解析为JSON对象然后取出name和message,并构建代表API返回的User实例,非常简单。
    
    现在让我们来看看有趣的部分,也就是如何使用POP的方式从URL请求数据,并声称对应的User。首先,我们可以创建一个protocol来代表请求。对于一个请求,我们需要知道它的请求路径,HTTP方法,所需要的参数等信息。一开始这个协议可能是这样的:
    

    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 }
    

    }

    现在新建一个UserRequest来实现Request协议:
    

    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] = [:]
    

    }

    UserRequest中有一个未定义初始值的name属性,其他的属性都是为了满足协议所定义的。因为请求的参数用户名name会通过URL进行传递,所以parameter是一个空字典(因为要用get请求)。有了协议定义和一个满足定义的具体请求,现在我们需要发送请求。为了任意请求都可以通过同样的方法发送,我们将发送方法定义在Request协议扩展上:
    

    extension Request {
    func send(handler: @escaping (User?) -> Void) {
    // … send 的实现
    }
    }

    在send(handler:)的参数重,我们定义了可逃逸的(User?)-> Void,在请求完成后,我们调用这个handler方法来通知调用者是否完成,如果一切正常,则将一个User实例返回,否则传回nil。
    
    我们想要这个send方法对于搜游的Request都通用所以显然回调的参数不能是User。通过在Request协议中添加一个管理类型,我们可以将回调参数进行抽象。在Request最后添加:
    

    protocl Request {
    ···
    associatedtype Response
    }

    然后在UserRequest中,我们也相应的添加类型定义,以满足协议:
    

    struct UserRequest: Request {

    typealias Response = User
    }

    现在,我们来重新实现send方法,我们可以用Response代替具体的User,让send一般化。我们这里使用URLSession来发送请求
    

    extension Request {
    func send(handler: @escaping (Response?) -> Void) {
    let url = URL(string: host.appending(path))!
    var request = URLRequest(url: url)
    request.httpMethod = method.rawValue

        // 在示例中我们不需要 `httpBody`,实践中可能需要将 parameter 转为 data
        // request.httpBody = ...
    
        let task = URLSession.shared.dataTask(with: request) {
            data, res, error in
            // 处理结果
            print(data)
        }
        task.resume()
    }
    

    }

    通过拼接host和path,可以得到API的entry point。根据这个URL创建请求,进行配置,生成data task并将请求发送。剩下的工作就是将会掉中的data转换成为合适的对象类型。并调用handler同志外部调用者。对于User我们知道可以使用User.init(data:),但是对于一般的Response,我们还不知道如何将数据专为模型。我们可以在Request里定义一个pase(data:)方法,来要求满足该协议的具体类型提供合适的实现。这样一来,提供转换方法的任务就被下放到了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)
    }
    }

    有了data转换为Response的方法后,我们就可以对请求的结果进行处理了:
    

    extension Request {
    func send(handler: @escaping (Response?) -> Void) {
    let url = URL(string: host.appending(path))!
    var request = URLRequest(url: url)
    request.httpMethod = method.rawValue

        // 在示例中我们不需要 `httpBody`,实践中可能需要将 parameter 转为 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()
    }
    

    }

    现在我们做一下请求
    

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

    2.重构关注点分离
    虽然能够实现需求,但是上面的实现可以说非常糟糕。让我们看看现在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) {

    }
    }

    这里最大的问题在于,Request管理了太多的东西。一个Request应该做的事情应该仅仅是定义请求入口和期望的相应类型,而现在Request补光定义了host的值,还对如何解析数据了如指掌。最后send方法被绑死在了URLSession的实现上,而且是作为Request的一部分村子。这是很不合理的,因为这意味着我们无法在不更改请求的情况下更新发送请求的方式,它们被耦合在了一起,这样的结构让测试变得异常艰难,我们可能需要通过stub和mock的方式对请求拦截,然后返回构造的数据,这会用到NSURLProtocol的内容,或者是引入一些第三方的测试框架大大的增加了项目的复杂程度。在OC这一时期这可能是一个可选项,但是在Swift新时代,我们有好的多的方法来处理这件事情。
    
    首先我们将send(handler:)从Request中分离出来。我们需要一个单独的类型负责发送请求。这里机遇POP的开发方式,我们定义一个可以发送请求的协议:
    

    protocol Client {
    func send(_ r: Request, handler: @escaping (Request.Response?) -> Void)
    //编译错误原因是,Request是含有“关联类型”的协议,所以它并不能作为独立的类型来使用,我们只能够将它作为类型约束,来限制输入参数request。
    //关联类型(associatedtype):我不知道具体类型是什么,一些服从我的类,结构体,枚举会帮我实现这个细节
    }

    正确的方式
    

    protocol Client {
    func send(_ r: T, handler: @escaping (T.Response?) -> Void)
    var host: String { get }
    }

    除了使用<T:Request>这个范型方式意外,我们还将host从Request移动到了Client里,这是更适合他的地方。现在,我们可以把含有send的Request协议扩展删除,重新创建一个类型满足Client了。和之前一样,他将使用URLSession来发送请求:
    

    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()
    }
    

    }

    现在发送请求部分和请求本身分离开了,而且我们使用协议的方式定义了Client。除了URLSessionClient意外,我们还可以使用任意的类型来满足这个协议,并发送请求。这样网络层的具体实现和请求本身就不再想管了,我们之后在测试的时候会进一步看到这么做所带来的好处。
    
    现在还有一个问题,Request的pase方法。请求不应该知道如何解析得到的数据,这项工作应该叫个Response来做。而现在我们没有对Response进行任何限定。接下来我们将新增一个协议,满足这个协议的类型将知道如何将一个data转换为实际的类型:
    

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

    Decodable定义了一个静态的parse方法,现在我们需要在RequestResponse关联类型中为它加上这个限制,这样我们可以保证所有的Response都可以对数据进行解析,远了Request中的parse生命也就可以解除了。最终的Request协议
    

    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
    

    }

    修改User满足Decodable,并且修改上面URLSessionClient的解析部分的代码,让它使用Response中的parse 方法
    

    extension User: Decodable {
    static func parse(data: Data) -> User? {
    return User(data: data)
    }
    }

    struct URLSessionClient: Client {
    func send(_ 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) {

    }
    }
    }

    最后,将client中不再需要的host和parse等清理一下,一个类型安全,解耦合的面向协议的网络层就呈现在我们眼前了。想要调用UserRequest时,我们可以这样写
    

    URLSessionClient().send(UserRequest(name: “onevcat”)) { user in
    if let user = user {
    print(“(user.message) from (user.name)”)
    }
    }

    可以为URLSessionClient添加一个单例,或者为请求添加Promise的调用方式。在POP的组织下,这些改动都很自然,也不会牵扯到请求的其他部分。你可以用和UserRequest类型相似的方式,为网络层添加其他的API请求,只需要定义请求所必要的内容,而不用担心会触及网络方面的具体实现。
    
    3.网络层测试
    将Client生命为协议给我们带来了额外的好处,那就是我们不在局限于使用某种特定的技术(比如这里的URLSession)来实现网络请求。利用POP,你只是定义了一个发送请求的协议,你可以很容易的使用像是ADNetworking或者Alamofire这样的成熟的第三方框架来构建具体的数据并处理请求的低层实现。我们甚至可以提供一组“虚假”对请求的响应,用来进行测试。这和传统的stub&mock的方式在概念上时接近的,但是实现起来要简单得多,也明确的多。这个就是加载一个本地文件。
    
    4.可扩展性因为高度耦合,这种基于POP的实现为代码的扩展提供了相对宽松的可能性。我们刚才已经说过,你不必自行去实现一个完整的Client,而可以依赖于现有的网络请求框架,实现请求发送的方法即可。也就是说你也可以很容易的将某个正在使用的请求方式替换为另外的方式,而不会影响请求的定义和使用。类似的以使用任意的第三方JSON解析库,来帮助我们迅速构建模型类型,这仅仅只需要实现一个将Data转换为对应模型类型的方法即可。
    
    #使用协议帮助改善代码
    通过面向协议的编程,我们可以从传统的集成商解放出来,用一种更灵活的方式,搭积木一样对程序进行组装。每个协议专注于自己的功能,特别得益于协议扩展,我们可以减少类和集成带来的共享状态的风险,让代码更加清晰。高度的协议话有助于解耦合,测试以及扩展,而结合范型来使用协议,更可以让我们免于动态调用和类型转换的苦恼,保证了代码的安全性。
    
    
    这段代码用的playground

    import Foundation
    import PlaygroundSupport
    PlaygroundPage.current.needsIndefiniteExecution = true
    protocol Client {
    func send(_ r:T,handler:@escaping(T.Response?)-> Void)

    }
    protocol Decodable {
    static func parse (data:Data) -> Self?
    }
    struct User:Decodable {
    let name:String
    let message:String
    static func parse(data: Data) -> User? {
    return User.init(data:data)
    }
    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
    }

    }

    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}
    associatedtype Response:Decodable

    }
    struct URLSessionClient: Client {
    static func shareClient() ->URLSessionClient{

    return self.init()
    }
    func send<T: Request>(_ r: T, handler: @escaping (T.Response?) -> Void) {
        let url = URL(string: r.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 = T.Response.parse(data: data) {
                DispatchQueue.main.async { handler(res) }
            } else {
                DispatchQueue.main.async { handler(nil) }
            }
        }
        task.resume()
    }
    

    }
    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] = [:]
    typealias Response = User
    func phase(data: Data) -> User? {
        return User.init(data: data)
    }
    

    }
    URLSessionClient.shareClient().send(UserRequest.init(name: “onevcat”)) { (user) in
    if let user = user {
    print(“(user.name)from(user.message)”)
    }
    }

    “`

    展开全文
  • POP面向协议编程

    2019-09-26 09:36:53
    面向协议编程 传统面向对象的开发思维方式是将类中相似的功能抽取出来,组成一个基类,然后子类继承与基类,就可以调用父类拥有的方法,而不必每次都写相同的方法,而iOS中并不支持多继承,所以继承了父类,就不...

    面向协议编程

    • 传统面向对象的开发思维方式是将类中相似的功能抽取出来,组成一个基类,然后子类继承与基类,就可以调用父类拥有的方法,而不必每次都写相同的方法,而iOS中并不支持多继承,所以继承了父类,就不可以再继承与其它类。例如:下面例子LGAnimal继承与NSObject,LGMonkey继承与LGAnimal,Monkey就可以重写父类的方法,实现自己的功能,也可以直接调用父类的方法
    class LGAnimal: NSObject {
        var leg:Int {return 4}
        func run(){
            print("\(leg)只脚奔跑")
        }
        func eat(){
            print("肉")
        }
    }
    class LGMonkey: LGAnimal {
        override var leg: Int {return 2}
        override func eat() {
            print("?")
        }
    }
    override func viewDidLoad() {
            super.viewDidLoad()
            
            let monkey = LGMonkey()
            monkey.run()
            monkey.eat()
        }
    
    • 而面向协议开发就不需要
    • 首先定义一个协议比如LGProtocl,然后遵守自定义的协议,LGTeacher和LGStudent都遵守了LGProtocl协议,然后必须实现协议中的方法,否则会报错比如:Type ‘LGStudent’ does not conform to protocol ‘LGProtocl’
    protocol LGProtocl {
        /// 协议属性
        var name: String {get}
        /// 协议方法
        func sayHello()
    }
    
    /// LGTeacher结构体来实现协议
    struct LGTeacher: LGProtocl{
        var name: String
        func sayHello() {
            print("你好")
        }
    }
    
    struct LGStudent: LGProtocl {
        var name: String
        
        //    func sayHello() {
        //        print("别烦我,我要学习POP")
        //    }
    }
    
    let array: [LGProtocl] = [LGTeacher(name: "Cooci"),LGStudent(name: "Jz")]
            array.forEach{ $0.sayHello()}
            
    
    • 总结:面向协议开发更加灵活,通过协议+扩展实现一个功能,能够定义所需要的充分必要条件,不多也不少。这样就最大程度减少了耦合。使用者可以像搭积木一样随意组合这些协议,写一个class或struct来完成复杂的功能
    • 备注:struct和class的主要区别:
      • struct是值引用,而class是类型引用
      • struct没有继承的功能,class有继承功能
    展开全文
  • 他们也声明 Swift 是世界上第一个面向协议编程的语言。通过它的名字, 我们可能认为面向协议编程都是跟协议相关的; 然而, 这可能是一个错误的假定。面向协议编程不仅仅是关于协议; 实际上它不仅是编写程序的新方式, ...
  • 面向协议编程,喵神在博客中有介绍: 面向协议编程与 Cocoa 的邂逅 (上) 面向协议编程与 Cocoa 的邂逅 (下) Swift和OC的区别、面向协议编程和面向对象编程的优缺点,可以看下面这篇文章: 来一次有侧重点的...
  • 深入理解Swift 面向协议编程

    千次阅读 2019-07-20 13:12:31
    文章标题谈到了面向协议编程(下文简称 POP),是因为前几天阅读了一篇讲 Swift 中 POP 的文章。本文会以此为出发点,聊聊相关的概念,比如接口、mixin、组合模式、多继承等,同时也会借助各种语言中的例子来阐述我的...
  • Swift 【基于 Swift 面向协议编程

    千次阅读 2018-06-10 10:01:11
    “我们如何在每天的开发过程中使用面向协议编程?Natasha 回答了这个问题,并专门针对 POP 的实际应用开发给出了解决方案,包含视图,视图控制器和网络的实例。关注本篇在 App Builders CH 大会上的演讲,你将从面向...
  • Swift面向协议编程

    千次阅读 2015-08-27 16:42:27
    一、使用类的好处 1.封装性 2.抽象性 3.采用命名空间来避免冲突 ...二、在以往的面向对象编程中,只有类才能提供的 1.类的继承层次体系 2.类由于方法变量可重载所具有的可定制和重用性 在Swift中,可定制性
  • 面向协议与面向对象的区别

    千次阅读 2017-12-11 22:19:11
    面向对象的设计和面向协议的设计都使用了多态让我们使用同样的接口来跟不同的类型进行交互。在面向对象的设计中,我们使用了基类提供的接口来跟所有的子类进行交互。... 当我们谈到面向协议编程的时候应该从协议开始
  • 转载注明出处:http://blog.csdn.net/qxuewei/article/details/53945445 因为OC 的局限性, 使得iOS 开发组件化编程变得不可能,得益于面向对象语言的特性 (封装,继承,多态) 在我们熟悉的设计模式中渐渐形成统一的...
  • Android中面向协议编程的深入浅出http://blog.csdn.net/sk719887916/article/details skay编写说起协议,现实生活中大家第一感觉会想到规则或者约定,确实协议的本意就是一种约束,每个人事物都遵守的准则或法则,...
  • ios 面向协议编程资源

    2018-09-13 10:04:43
    WWDC视频
  • Android 面向接口编程

    千次阅读 2017-07-04 19:42:00
    关键词:Android、POP、面向接口编程 、面向过程、面向协议一、概述面向接口编程是面向对象编程的一种实现方式,它的核心思想是将抽象与实现分离,从组件的级别来设计代码,达到高内聚低耦合的目的。最简单的面向...
  • 本期 fir.im Weekly 重点推荐关于 iOS 面向协议编程相关文章,还有 iOS 多线程安全、Swift 进阶、Android MVVM 应用框架、Android 蓝牙实践等技术文章分享和工具源码分享~『iOS / Android开发分享 』面向协议编程与...
  • iOS --- 面向协议编程(swift2.3)

    千次阅读 2016-07-04 14:28:37
    面向协议编程 协议聚合 泛型约束 swift是面向协议的编程语言 UIKit中的委托模式 创建自己的委托模式 可选的协议方法 一:扩展协议和默认实现 protocol Record: CustomStringConvertible{ var wins: Int {get} var ...
  • 接口是一系列可调用方法的集合。...接口编程是指当写一个函数或一个方法时,我们应该更加关注具体的...在OC中,接口又可以理解为Protocol,面向接口编程又可以理解为面向Protocol编程,或者面向协议编程。在Swif
  •  引言--面向接口所处的设计模式中的位置。...但基本上,面向接口和面向实现都基于面向对象的模式,也就是说面向接口并不能称为比面向对象的更高的一种编程模式。而是在面向对象中大的背景下的
  • 基于面向协议MVP的介绍 MVP实战开发 说在前面: 相信就算你是个iOS新手也应该听说过MVC的,MVC是构建iOS App的标准模板。随着时间的推移,在iOS平台上MVC也逐渐开始面临着越来越多的问题,最近又开始流行MVVM,MVVM使...
1 2 3 4 5 ... 20
收藏数 143,754
精华内容 57,501
关键字:

面向协议编程