面向协议编程(Protocol Oriented Programming,简称POP)是Swift的一种编程范式,Apple于2015年WWDC提出。在Swift的标准库中,能见到大量POP的影子。
一、POP和OOP
1.1. 回顾OOP
Swift也是一门面向对象的编程语言(Object Oriented Programming,简称OOP)。
OOP的三大特性:封装、继承、多态。
继承的经典使用场景:当多个类(比如A、B、C类)具有很多共性时,可以将这些共性抽取到一个父类中(比如D类),最后A、B、C类继承D类。
有些问题,使用OOP并不能很好解决,比如:如何将BVC、DVC的公共方法run
抽取出来?
class BVC: UIViewController {
func run() {
print("run")
}
}
class DVC: UITableViewController {
func run() {
print("run")
}
}
1.2. OOP解决方案
-
将
run
方法放到另一个对象A中,然后BVC、DVC
拥有对象A属性; -
将
run
方法添加到UIViewController
分类中(UITableViewController
继承自UIViewController
); -
将
run
方法抽取到新的父类,采用多继承(C++支持多继承)。
虽然可以解决问题,但是第一种方法多了一些额外的依赖关系。第二种方法会导致UIViewController
越来越臃肿,而且会影响它的其他所有子类。第三中虽然在iOS开发中用不到,但在C++中使用也会增加程序设计的复杂度,产生菱形继承等问题,需要开发者额外解决。
1.3. POP解决方案
定义一个协议,同时定义公共方法run
。扩展协议并实现抽象方法run
,类遵守协议即可。
protocol Runnable {
func run()
}
extension Runnable {
func run() {
print("run")
}
}
class BVC: UIViewController, Runnable {
}
class DVC: UITableViewController, Runnable {
}
相比较OOP,POP的解决方案更加简洁。因为协议支持扩展实现,使得Swift使用POP非常便捷。
在Swift开发中,OOP和POP是相辅相成的,任何一方并不能取代另一方。POP能弥补OOP一些设计上的不足。
1.4. POP的注意点
- 优先考虑创建协议,而不是父类(基类)
- 优先考虑值类型(
struct、enum
),而不是引用类型(class
) - 巧用协议的扩展功能
- 不要为了面向协议而使用协议(有时候使用类更合理)
二、添加前缀
场景:为字符串增加计算纯数字的功能。
示例代码:
var str = "123ttt456"
func numberCount(_ str: String) -> Int {
var count = 0
for c in str where ("0"..."9").contains(c) {
count += 1
}
return count
}
print(numberCount(str)) // 输出:6
上面的示例代码已经完成了基本的功能,但是还有很多需要优化的地方。
优化代码一(协议扩展):
extension String {
func numberCount() -> Int {
var count = 0
for c in self where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(str.numberCount()) // 输出:6
使用协议后,所有字符串都可以使用该功能,并且也限定了只有字符串类型可以使用。但是计算属性比函数调用更加优雅。
优化代码二(计算属性):
extension String {
var numberCount: Int {
var count = 0
for c in self where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(str.numberCount) // 输出:6
print("45579test12".numberCount) // 输出:7
上面优化过后看似已经很完善了,但隐藏一个风险:属性名有可能和系统有冲突。这时候我们可以为属性添加一个前缀加以区分。
优化代码三(OC风格加前缀):
extension String {
var db_numberCount: Int {
var count = 0
for c in self where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(str.db_numberCount) // 输出:6
类似OC的前缀做法虽热能够满足条件,但总感觉有点OC的风格。其实我们可以使用Swift风格为属性/函数添加前缀。
优化代码四(Swift风格加前缀):
struct DB {
var string: String
init(_ string: String) {
self.string = string
}
var numberCount: Int {
var count = 0
for c in string where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
extension String {
var db: DB {
return DB(self)
}
}
print(str.db.numberCount) // 输出:6
为字符串扩展一个DB
(自定义)属性,DB
是自定义结构体,内部实现了字符串数字计数功能。这样不仅拥有自己的命名空间,后续有任何字符串相关自定义功能都可以放到结构体中。
一般情况下,我们不会只为字符串扩展自定义功能,数组、字典、自定义类等都有可能扩展新的自定义功能。但总不能为在DB
结构体中为每一个类型都新增一个类型和初始化方法,这样DB
结构体就会变得很臃肿。可以使用泛型解决该问题。
优化代码五(通用):
struct DB<Base> {
var base: Base
init(_ base: Base) {
self.base = base
}
}
extension String {
var db: DB<String> {
DB(self) }
}
class Person {
var name: String
init(_ name: String) {
self.name = name
}
}
extension Person {
var db: DB<Person> {
DB(self) }
}
extension DB where Base == String {
var numberCount: Int {
var count = 0
for c in base where ("0"..."9").contains(c) {
count += 1
}
return count
}
}
print(str.db.numberCount) // 输出:6
extension DB where Base: Person {
func run() {
print(base.name, "running")
}
}
let p = Person("大奔")
print(p.db.run()) // 输出:大奔 running
使用泛型后,再为结构体DB
扩展对应类型的函数/属性,不仅类型隔离,还做到了统一DB
管理。
注意:为类扩展时要注意扩展方法是否要求子类也必须拥有,
Base: Person
和Base == Person
是有区别的。
如果要为类扩展属性/方法,就需要再增加一个类型属性db
。
示例代码:
extension String {
var db: DB<String> {
DB(self) }
static var db: DB<String>.Type {
DB<String>.self }
}
extension DB where Base == String {
var numberCount: Int {
var count = 0
for c in base where ("0"..."9").contains(c) {
count += 1
}
return count
}
static func test() {
print("String test")
}
}
String.db.test() // 输出:String test
如果为不同类型扩展方法,就需要重复定义实例属性和类型属性。这时候可以使用协议实现这些属性,那个类型需要扩展方法,只需要遵守该协议即可。
最终优化代码:
// 1. 前缀类型(中介)
struct DB<Base> {
var base: Base
init(_ base: Base) {
self.base = base
}
}
// 2. 利用协议扩展前缀属性
protocol DBCompatible {
}
extension DBCompatible {
var db: DB<Self> {
set {
}
get {
DB(self) }
}
static var db: DB<Self>.Type {
set {
}
get {
DB<Self>.self }
}
}
// 3. 让String拥有db前缀属性
extension String: DBCompatible {
}
// 4. 给db前缀(实例/类型)扩展功能
extension DB where Base == String {
var numberCount: Int {
var count = 0
for c in base where ("0"..."9").contains(c) {
count += 1
}
return count
}
static func test() {
print("String test")
}
}
print(str.db.numberCount) // 输出:6
String.db.test() // 输出:String test
// 为自定义类扩展方法
class Person {
var name: String
init(_ name: String) {
self.name = name
}
}
extension Person: DBCompatible {
}
extension DB where Base: Person {
func run() {
print(base.name, "running")
}
}
let p = Person("大奔")
print(p.db.run()) // 输出:大奔 running
注意:如果扩展函数使用
mutating
,在利用协议扩展前缀属性时,属性必须是可读可写的。
为String
增加扩展功能后,NSString
和NSMutableString
也能使用么?
需要为NSString
和NSMutableString
扩展db
属性,但其实只需要为NSString
扩展就行了,因为NSMutableString
继承自NSString
。
extension NSString: DBCompatible {
}
找不到对应方法,但是如果为其扩展一个和String
方法就会出现代码重复。我们知道String
、NSString
和NSMutableString
都遵守了一个ExpressibleByStringLiteral
协议,只需要把扩展条件修改一下就可以了。
extension DB where Base: ExpressibleByStringLiteral {
var numberCount: Int {
var count = 0
for c in (base as! String) where ("0"..."9").contains(c) {
count += 1
}
return count
}
static func test() {
print("String test")
}
}
注意base
需要强转为String
。
三、利用协议实现类型判断
场景:判断传入的实例参数是不是数组。
示例代码:
func isArray(_ value: Any) -> Bool {
value is [Any]
}
print(isArray([1, 2])) // 输出:true
print(isArray(["1", 2])) // 输出:true
print(isArray(NSArray())) // 输出:true
print(isArray(NSMutableArray())) // 输出:true
print(isArray("123")) // 输出:false
场景:判断传入的类型参数是不是数组。
示例代码:
protocol ArrayType {
}
extension Array: ArrayType {
}
extension NSArray: ArrayType {
}
func isArrayType(_ type: Any.Type) -> Bool {
type is ArrayType.Type
}
print(isArrayType([Int].self)) // 输出:true
print(isArrayType([Any].self)) // 输出:true
print(isArrayType(NSArray.self)) // 输出:true
print(isArrayType(NSMutableArray.self)) // 输出:true
注意:[Int].Type
和[Any].Type
没有关系。协议最终是一个类型,是有自己的meta
的。
转载:https://blog.csdn.net/yangbenben8866/article/details/116517892