Swift--面向协议编程POP

1,919 阅读9分钟

前言

面向协议编程 (Protocol Oriented Programming,以下简称 POP),是苹果在2015WWDC 上提出的Swift的一种全新理念编程范式。被誉为可以改变一切的编程方式。本篇只做为记录自己的学习过程。

面向对象编程

在学习POP之前,先来简单回顾一下面向对象编程:

相信所有的iOS开发者都了解面向对象编程(Object-oriented Programming,以下简称:OOP)。在面向对象编程世界里,一切皆为对象,对象是程序的基本单元,对象把程序与数据封装起来提供对外访问的能力,提高软件的重用性,灵活性和扩展性。在面向对象编程中,通常把对象的数据称为属性,把对象的行为称为方法。

面向对象编程包含三个重要的概念:

1️⃣::具有相同特征和行为的事物的抽象,如人类,动物类

2️⃣:对象:对象是类的实例,万物皆对象

3️⃣:方法:方法是对象的具体行为

下面通过一个简单的图来看一下:

如果把上面图片的内容转换成代码:

class ZHPerson {
        func eat() {
            print("eat hamburg")
        }
    }
    
class ZHMan: ZHPerson {
        var game:String?
        
        func driver() {
            print("Old driver")
        }
        override func eat() {
            print("instant noodles")
        }
    }
    
class ZHWoman: ZHPerson {
        var bag:String?
        
        func shopping() {
            print("Brush my card")
        }
        override func eat() {
            print("beefsteak")
        }
    }

在上面代码例子🌰中,子类ZHManZHWoman共享了父类ZHPersoneat方法,而这部分代码被封装到了父类 ZHPerson 中,这里就用到了面向对象编程的核心思想特征:封装与继承。

面向对象编程的三大特征:封装继承多态

封装(Encapsulation)

通过对象来隐藏程序的具体实现细节,将数据与操作封装在一起,对象与对象之间通过消息传递机制实现互相通信,具体的表现就是通过提供访问接口实现消息的传入传出。 封装常常会通过控制访问权限来控制对象之间的互访权限,常见的访问权限:公有(public),私有(private),保护(protected)。

封装的意义:由于封装隐藏了具体的实现,如果实现的改变或升级对于使用方而言是无感知的,提高程序的可维护性;而且封装鼓励程序员把特定数据与对数据操作的功能打包在一起,有利于应用程序的去耦。

继承(Inheritance)

继承即类之间可以继承,通过继承得到的类称为子类,被继承的类为父类,子类相对于父类更加具体化。 子类具有自己特有的属性和方法,并且子类使用父类的方法也可以覆盖(重写)父类方法,在某些语言中还支持多继承,但是也带来了覆盖的复杂性。

继承的意义:继承是代码复用的基础机制

多态(Polymorphism)

多态发生在运行期间,即子类型多态,指的是子类型是一种多态的形式,不同类型的对象实体有统一接口,相同的消息给予不同的对象会引发不同的动作。

多态的意义:提供了编程的灵活性,简化了类层次结构外部的代码,使编程更加注重关注点分离

随着时代的进步,国家的强盛,科技的发展,人们生活水平的提高,ZHMan在拥有driver方法的基础上,也想要拥有shopping方法,同样的ZHWoman在拥有shopping方法的情况下,加入了driver方法,可是他们本身并不具有这些方法,那么该怎么获取到这些本身并不具本,只在别的子类存在的方法呢? 在面向对象编程中,有如下几个解决方法:

方法1️⃣:Copy & Paste,给继承于ZHPerson的子类ZHMan拷贝一份shopping方法,不好。

方法2️⃣:基类,给基类ZHPerson添加一个shopping方法,这样子类ZHMan就具有了个shopping功能,但是这样会使其他不需要shopping功能的子类也具有了这个方法,不好。

方法3️⃣:依赖注入,通过外界传入一个带有 shopping方法 的对象,用新的类型来提供这个功能。这是一个稍好的方式,但是引入额外的依赖关系,有耦合,不好。

方法4️⃣:多继承,然而Swift 是不支持多继承的。不过如果Swift有多继承的话,确实可以从多个父类进行继承,并将 shopping方法 添加到合适的地方。但是此时会带来另外一个问题:菱形缺陷,即如果继承的两个父类都有同样的方法,子类就很难确定继承的到底是哪个父类的方法。

以上就是面向对象编程在实际解决问题的时候所暴露出的问题,虽然通过个人技能总能解决,但感觉就是不那么干净利索。

面向协议编程

抛开我们熟悉的 swift 标准库核心是面向协议不说,就连一些函数响应式编程框架,像RxSwift也是面向协议的。面向协议编程到底是什么呢?一句话就能概括:用协议扩展的方式代替继承,实现代码复用。

1.协议

首先来创建一个最简单的Swift协议:

protocol People {
    var name: String {get set }
    func shopping()
}


struct Student:People {
        var name: String
        
        func shopping() {
            print("more money")
        }
}
//调用
Student(name: "King").shopping()

上面👆👆👆代码中定义名为People协议,包含一个name属性,以及一个shopping方法的定义 ,Studet 结构体通过实现name属性 和 shopping方法 来满足 People协议。在调用时就可以使用 People 中定义的方法了。

如果此时在多一个结构体Teacher,

struct Teacher:People {
        var name: String
        
        func shopping() {
            print("more money")
        }
}

此时需要再次实现shopping方法,因为在协议里没有提供方法的实现, 那么这样一来每多一个类继承协议,都需要再次实现shopping方法,那么这种操作不就和面向对象编程中的 Copy & Paste 的解决方式有些类似了吗?

2.协议扩展

那么如果为协议提供默认的实现方法,也就是协议扩展,是不是就解决了这个问题?

protocol People {
    var name: String {get set }
    func shopping()
}

extension People {
    func shopping() {
        print("more money")
    }
}

struct Student:People {
    var name: String
}

struct Teacher:People {
    var name: String
}
//调用
Student(name: "King").shopping()
Teacher(name: "Queue").shopping()

通过协议拓展给协议的遵循者一些默认的方法、属性的实现。符合协议的类型可以提供自己的实现,也可以使用默认的实现。同时也可以在协议的扩展中添加协议中未声明的附加方法实现,并且实现协议的任何类型都可以使用到这些附加方法。这样就可以给遵循协议的类型添加特定的方法。

3.协议继承

在协议的世界中,我们可以使用面向对象编程中的继承概念对协议进行继承,形成协议树。但是需要注意的是这同样会带来面向对象编程中的缺陷,不宜滥用。 下面在People协议的基础上实现Human协议:

protocol Human:People {
    var hobby: String {get set }
    func driver()
}

接着来实现一个结构体:

struct Specialist: Human {
    var hobby: String
    var name: String
    func driver() {
       print("360km/h")
    }
    func shopping() {
        print("100 million $")
    }
    
}

同样的在Specialist结构体内不仅要实现Human协议的属性和方法,也要实现People协议的属性和方法,这里对这两个协议中的方法拓展就不在一一赘述了。

4.协议组合

协议的组合就类似于面向对象编程中的多继承,采用多个协议的默认实现。

protocol People {
    var name: String {get }
    func shopping()
}

extension People {
    func shopping() {
        print("more money")
    }
}

protocol Human {
    var name: String {get }
    func driver()
}

extension Human {
    func driver() {
        print("360km/h")
    }
}

这里我们声明协议People和协议Human,在协议中都声明了一个name属性,并对其方法进行扩展。 下面我们实现一个结构体:

struct Superman: People, Human {
    var name: String
}
//
Superman(name: "Jing")

Superman结构体必须实现一个name属性来满足这两个协议,如果此时,把People协议中的name属性默认实现会怎么样?

extension People {
    var name: String{
        return "hahahahha"
    }
    func shopping() {
        print("more money")
    }

}

struct Superman: People, Human {
//    var name: String
}

//
Superman()

依然可以,甚至此时在Superman结构体内不需要实现name属性,如果此时再把Human协议中的name属性默认实现的话,结果就没有那么美好了。

这种情况下,Superman 无法确定要使用哪个协议扩展中 name属性 的定义。在同时实现两个或者以上含有同名属性的协议,并且它们都提供了默认扩展实现时,我们需要在具体的类型中明确地提供属性实现。

struct Superman: People, Human {
    var name: String
}
//
Superman(name: "King")

总结

面向对象编程和面向协议编程最明显的区别在于程序设计过程中对数据类型的抽象上,面向对象编程使用类和继承的手段,而面向协议编程使用的是遵守协议的手段。面向协议编程是在面向对象编程基础上发展而来的,而并不是完全背离面向对象编程的思想。 面向协议编程的好处在于,通过协议+扩展实现一个功能,这样就最大程度减少了耦合。

可能会有人觉得疑惑,那这和面向接口编程有什么区别?依稀记得,接口只是一种约束,而非一种实现,也就是说实现了某个接口的类,需要自己实现接口中的方法。但是Swift 为协议提供了拓展功能,它能够为协议中的方法提供默认实现,不需要写重复的代码。(个人理解,如有错误,还请指正,谢谢您! 最后感谢喵神)。