Swift POP 可以使你的代码更优雅

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在创造这种语言的最核心的思想。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容

  • 基于 Swift 的面向协议编程 当 Swift 刚刚出现的时候,学习新东西都是令人兴奋的。第一年,我很高兴能学习...
    wanqijian阅读 332评论 1 1
  • 此文为资料汇总文,基于自己的理解收集网络上简明易懂的文章及例子,通篇浏览之后会对这个概念会有初步的认识。 参考资料...
    ChrisPzzz阅读 5,905评论 4 20
  • 对面向协议不熟悉的swift开发者,个人感觉这篇文章写得很好,适合面向协议编程的初学者。原文作者:http://w...
    说不出情绪阅读 1,000评论 0 6
  • 需求:假设你要写一个由一张图片和一个按钮构成的简单应用,产品经理希望按钮被点击的时候图片会抖动 实现1:写一个 U...
    大宝的爱情阅读 481评论 0 0
  • Objective-C协议: OC中protocol的作用仅仅就是声明方法,然后交给任何类去实现 当我们想为一个协...
    Simple_Code阅读 1,127评论 0 1