POP 全称 Protocol Oriented Programming, 面向协议编程。
所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是"关于实现的约定"。
protocol Codeable {
var name: String?
func coding()
}
Swift是一门面向对象的语言,类已经满足我们所有的需求,功能也十分强大。为什么还要使用POP?
首先在Swift中,值类型优先于类。然而,面向对象的概念不能很好地与结构体和枚举一起共存, 因为结构体和枚举不能够被继承。因此,作为面向对象的一大特征—继承就不能够应用于值类型了。
Swift的 POP 是使用了继承的思想,它模拟了多继承关系,实现了代码的跨父类复用,同时也不存在 is-a 关系。swift中主类和 extension扩展类 的协同工作,保留了 在主类中定义方法 在所继承的类中进行实现的特性,又新增了在 extension拓展类 中定义并实现的方法在任何继承自此协议的类中可以任意调用,从而实现组件化编程。
让我们假设你的产品经理过来和你说,“我们在点击那个按钮时候出现一个视图,而且它会抖动。” 这是一个非常常见的动画,比如,在你的密码输入框上 – 当用户输入错误密码时,它就会抖动。
一些人可能已经有了 Swift 抖动对象的基础代码。一些人甚至都有 Swift 的抖动对象的代码,我想都不用想,只要稍稍修改一下。
// ShakeImageView.swift
import UIKit
class ShakeImageView: UIImageView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 4.0, y: self.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 4.0, y: self.center.y))
layer.add(animation, forKey: "position")
}
}
我将创建一个 UIImageView 的子类,创建我的 ShakeImageView 然后增加一个抖动的动画:
ViewController.swift
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var ShakeImageView: ShakeImageView!
@IBAction func onShakeButtonTap(sender: AnyObject) {
shakeImageView.shake()
}
}
在我的 view controller 里面,在 interface builder 里我连接我的 view,把它做为 ShakeImageView 的子类,我有一个 shake(),然后 完成了!我的代码工作得很正常。
然后,你的产品经理过来说,”你需要在抖动视图的时候抖动按钮。” 然后我回去对按钮做了同样的事情。
// ShakeButton.swift
import UIKit
class ShakeButton: UIButton {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 4.0, y: self.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 4.0, y: self.center.y))
layer.add(animation, forKey: "position")
}
}
新建UIButton子类 ShakeButton,增加一个 shake() 函数。现在我能抖动我的 ImageView 和 Button 了,完成了。
// ViewController.swift
class ViewController: UIViewController {
@IBOutlet weak var shakeImageView: ShakeImageView!
@IBOutlet weak var shakeButton: ShakeButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
幸运的是,这会给你一个警告:我在两个地方重复了抖动的代码。如果我想改变抖动的幅度,我需要改两处代码,这很不好。
话说ImageView UIButton 这两个玩意儿不都是UIView的子类么,一拍大腿, 于是,灵机一动!
作为一个优秀的程序员,我们马上会意识到这点。如果你以前使用过 Objective-C,我会创建一个 UIView 的类别,在 Swift 里面,这就是扩展(Extension), 对UIView 进行扩展。
我能这样做,因为 UIButton 和 UIImageView 都是 UI 视图。我能扩展 UI 视图而且增加一个 shake 函数。现在我仍然可以给我的按钮和图像视图都加上其他的逻辑,但是 shake 函数就到处都是了。
// UIViewExtension.swift
import UIKit
extension UIView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 4.0, y: self.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 4.0, y: self.center.y))
layer.add(animation, forKey: "position")
}
}
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: ShakeImageView!
@IBOutlet weak var actionButton: ShakeButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
马上我们就能发现可读性很差了。例如,对于 shakeImageView 和 shakeButton 来说,你看不出来任何抖动的意图。整个类里面没有任何东西能告诉你它需要抖动。这样表意不明确,因为别处可能会随机存在一个抖动函数,你甚至不知道它是从哪里来的,曾经自己犯的错。
最主要的是,如果你常常为类别和 UIView 的扩展这样做的话,你可能会有更好的办法。在你增加了 shake()的地方,就成了所谓的 科学怪人的垃圾场。然后有人来和你说, “我想要一个可调暗的视图”,然后你增加一个 dim 函数和其他别处随机的调用函数。这样,代码文件文件就会变成一个很长的垃圾文件,因为这些随机调用的事情都是在 UIView 里面完成,尽管有些时候也许只有一两个地方需要这么做,这导致代码变得不可读,难以查看。我们该如何改变这点呢?
主角开始登场,我们当然会用到协议。首先我们创建一个 Shakeable 的协议:
// Shakeable.swift
import Foundation
import UIKit
protocol Shakeable { }
extension Shakeable where Self: UIView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 4.0, y: self.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 4.0, y: self.center.y))
layer.add(animation, forKey: "position")
}
}
在协议扩展的帮助下,你可以把它们限制在一个特定的类里面。在这个例子里面,我能抽出我的 shake(),然后用类别,我能说这是我们需要遵循的唯一的东西,只有 UI 视图会有这个函数。
你仍然可以使用你原来想用的同样强大的扩展功能,但是你有协议了。任何遵循协议的非视图不会工作。只有视图才能有这个 shake 的默认实现。
class ShakeImageView: UIImageView, Shakeable {
}
class ShakeButton: UIButton, Shakeable {
}
我们可以看到 ShakeImageView 和 ShakeButton 会遵循 Shakeable 协议。它们会有 shake(),现在的可读性强多了 –- 我可以理解 shaking 是有意存在的。如果你在别处使用视图,我需要想想,”在这也需要抖动吗?”。它增强了可读性,但是代码还是闭合的和可重用的。
假设我们想抖动和调暗视图。我们会有另外一个协议,一个 Dimmable 协议,然后我们可以为了调暗做一个协议扩展。再强调一遍,通过看类的定义来知晓这个类的用途,这样意图就会很明显了。
class FoodImageView: UIImageView, Shakeable, Dimmable {
}
关于重构,当产品说 “我不想要抖动了” 的时候,你只需要删除相关的 Shakeable 协议就好了。
class FoodImageView: UIImageView, Dimmable {
}
在你想添加业务的地方调用此协议:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var shakeButton: ShakeButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func didClickShakeButton(_ sender: Any) {
shakebutton.shake()
}
}
基于POP 思想的面向协议编程是不是可以很优雅
例如: 我们使用XIB加载的自定义View都会使用相同的方法:
Bundle.main.loadNibNamed("RedView", owner: nil, options: nil)?.last
通常在自定义View的实现中定义一个类方法。
例如:
class func loadWithNib() -> RedView {
return Bundle.main.loadNibNamed("RedView", owner: nil, options: nil)?.last! as! RedView
}
如果我们定义一个可从XIB加载某类的协议,在自定义类中仅仅需要遵守这个协议,无需实现协议中的方法,便可实现此协议中实现的所有方法.例如定义一个 可从XIB加载的协议 NibLoadable。
import Foundation
protocol NibLoadable {
}
extension NibLoadable where Self: UIView {
static func loadViewWithNib() -> Self {
return Bundle.main.loadNibNamed("\(self)", owner: nil, options: nil)?.last! as! Self
}
}
只要在xib加载的类中继承此协议。
import UIKit
class GreenView: UIView , NibLoadable {
}
就可以在需要初始化此对象时直接调用。
GreenView.loadViewWithNib()
这种类似于插件化的 POP 思想,能确实能解决现实中的代码层次结构混乱的问题, 并且苹果鼓励我们使用面对协议的思想, 这也是Chris Lattne在创造这种语言的最核心的思想。