Swift 面向Protocol编程浅析:Inheritence-Composition-Protocol

记得从大学学编程开始,对于软件编程听的最多的就是面向对象编程(Object Oriented Programming,OOP)了,它的三大特征:封装,继承,多态.而Swift倡导的面向协议编程(Protocol-oriented programming,POP)是OOP的一个范例,我理解为"封装+协议*结构体+扩展"(Swift2.0开始,你可以扩展一个protocol)

WWDC:Protocol-Oriented Programming in Swift开头Classess Are Awesome,指出OOP中的Classes提供了:数据的封装、访问控制、抽象化、命名空间等,但这些都是Classes才特有的属性吗?事实上,这些都是类型(Type)的所有属性,Classes只是Type的一种实现方法,在Swift中I can do all that with structs and enums(Swift的标准库组成:55个Protocols和102个Structs),这一点可以理解为封装性.而继承和多态,是structenum不具备的,它则是通过遵守protocol来实现.

但,这些是为了说明让我们放弃OOP吗?这是不可能的....想想UIKit....记得刚开始用Swift写项目时,总是告诫自己,不能只是机械的把objc 翻译成 swift.<font color="IndianRed">实际开发项目中,ViewControlView基本都是使用系统的框架,通过继承来实现,无论如何的自定义,都是要围绕苹果的那一套来,OC与swift在这一块保持一致;但在modelhandle/viewModel/manager这一块,更多的通过POP实现,后面会通过一个例子来说明.</font>(PS:在oc中,我们体会的是OOP/FRP(参考一下RAC),那Swift就是OOP/FRP(参考一下RxSwift)/POP;在oc中对于Protocols的理解更多的是UIAppplicationDelegate,UITableViewDelegete,NSCopying,UITextFieldDelegate....,而Swift中Protocols则被赋予了更多的功能和意义:"可定义属性,可组合,可继承,可扩展,支持泛型,支持类/结构体/枚举").在Swift面向协议编程初探中,bz总结的一句话,非常nice:
面向对象编程和面向协议编程最明显的区别在于程序设计过程中对数据类型的抽取(抽象)上,面向对象编程使用类和继承的手段,数据类型是引用类型
面向协议编程使用的是遵守协议的手段,数据类型是值类型(Swift中的结构体或枚举)
PS:值类型引用类型的区别这里不作详叙,可参考Swift:什么时候使用结构体和类

讨厌的"上帝"(Inheritence)

继承带给我们最大的问题,可能就是常常会构造出所谓的God类/super类,带来的坏处也随之可见:

  • 一层一层一层的传递下去,它的任何行为都会影响它的所有小弟;
  • 有的小弟继承了无用的属性和方法;
  • 不方便扩展,差别不大的同类上帝,直接拷贝一遍代码?
    特别喜欢田伟宇博客:跳出面向对象思想(一) 继承中提到关于继承的要点之一:父类的所有变化,都需要在子类中体现,也就是说此时耦合已经成为需求.(他的文章非常nice,在架构这一块的写的系列文章值得深读)so,LZ的观点也是万不得已不要用继承,优先考虑组合!
    注:在objc中,更多的是用组合(Composition),在Swift中则是协议>组合>继承.后面会举例说明.
    再注:全文的Demo在这里
    我们通过两张图对比一下:引用自程序员聊人生

// 父类
class Animal {
    var name: String = ""
    var type: String = ""
    func eat(){} 
//    func fly(){}
}

class Bird: Animal {
    func fly(){
        print("Bird can fly")
    }
}

class Preson: Animal {
    func speak(){
        print("person can speak")
    }
}

class Fish: Animal {
    func swimming() {
        print("fish can swimming")
    }
}

// 假设超人会飞不会游泳,复制飞的方法
class SuperMan: Preson {
    override func speak() {
        print("superman also speak")
    }
    func fly(){
        print("superman also fly")
    }
}

class SuperFishMan: SuperMan {
    func swimming() {
        print("superfishman can swimming")
    }
}
  • objc/Swift都不存在多继承,会游泳的超人,这时要复制游的方法,到这里已经是第四层了...高耦合
  • 也不好直接把fly()定义到父类Animal中,等于强加限制.因为通过继承,抽象出共同的性质,Bird/Preson/Fish都是动物(人是高级动物),它们都有属性name和type,都具有eat()的行为,但fly()不是所有动物共有的
  • 这时来了一个外星生物,它不属于Animal,但是拥有Animal及其子类所有的属性和行为(方法),怎么办?上帝类都帮不了你了,又走上了重复复制之路!

有句话是咋说的:我们区分鸟和鱼,不是因为它们的名字是鸟/鱼,而是通过它们表现的行为,有点乱,_.把所有的行为拆分出来,通过搭积木的形式组合出来,你具备什么就拿什么,那么你的身份也就随之浮现了.

protocol Property {
    var name: String {get}
    var type: String {get}
}

extension Property {
    var name: String {
        return "超人"
    }
    var type: String {
        return "外星类"
    }
}

protocol Speaker {
    func speak()
}

protocol Flyer {
    func fly()
}

protocol Swimer {
    func swimming()
}

struct SuperMan {
}

extension SuperMan: Property,Flyer,Speaker {
    func fly() {
        print("superman also fly")
    }
    func speak() {
        print("superman also speak")
    }
}

struct SuperFishMan {
}

extension SuperFishMan: Property,Flyer,Speaker,Swimer {
    var name: String {
        return "超水人"// 好蠢的名字...
    }
    func fly() {
        print("...")
    }
    func speak() {
        print("...")
        
    }
    func swimming() {
        print("....")
    }
}
  • objc中,还是可以定义一个父类Animal的,LZ现在基本都是写Swift了,就直接定义了一个protocol:Property,在扩展中写好默认实现
  • 消灭了上帝类,全部都定义为protocol,用到什么就拼接什么,真的就够搭积木一样便捷...

组合(Composition),哎哟不错
第一个例子属于对于model定义,接下来看一个view层所表现出的问题.
手机QQ底部tabbar的三个标签首页都带有一个头像控件,最开始我们采取继承的形式来实现一个baseVC

class KQUserAvatarView: UIView {
}

class KQBaseViewController: UIViewController {
    var userAvatarView: KQUserAvatarView!

    func setupUserAvatarView() {
    }
    
    func clickOnAvatarView() {
    }
}
  • 新需求,希望第一、第二个标签页的头像加上大V的标志,第三页保持不变,此刻高耦合,父类改动牵动三个子类/甚至更多子类的变化.或许你直接在父类中添加改变样式的方法,那么那些不需要改变的子类也就直接继承了无用的方法...
  • 又来个需求,我需要一个父类是UITableViewController的新KQUserAvatarView,瞬间傻眼...只能复制代码再创造一个上帝了
  • 这种情况还只是一个view的创建,如果是好几个组合view的组成,那么VC中的代码简直就是灾难...

在objc/Swift1.2之前的,我们用组合来代替继承,这是非常常见的一种做法.借助中间件,解耦+转移逻辑代码,减轻VC的负担.

class KQUserAvatarView: UIView {
    var btn: UIButton!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

typealias ClickButtonAction = () -> ()
class KQUserAvatarViewManager {
    var userAvatarView: KQUserAvatarView!
    var tapHandle: ClickButtonAction?
    
    func setupUserAvatarViewAtContainView(view: UIView,tapHandle: ClickButtonAction?) {
        userAvatarView = KQUserAvatarView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        userAvatarView.backgroundColor = UIColor.orangeColor()
        view.addSubview(userAvatarView)
        self.tapHandle = tapHandle
        userAvatarView.btn.addTarget(self, action: "clickOnAvatarView", forControlEvents: .TouchUpInside)
    }
    
    func clickOnAvatarView() {
        if let block = self.tapHandle {
            block()
        }
    }
}

class ViewController: UIViewController {
    var manager: KQUserAvatarViewManager!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        manager.setupUserAvatarViewAtContainView(self.view) {
            print("点击了按钮")
        }
        
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
  • 哪个页面需要头像控件,直接创建一个KQUserAvatarViewManager对象进行引用就行,实现了解耦
  • 多个view对应对个manager,很好的给VC进行了瘦身
  • 题外话:前端时间看了阳神的iOS 开发中的 Self-Manager 模式,评论也看了...对于文章所诉的观点,LZ我也是赞同评论中提议的创建一个ViewManager,在它里面处理点击事件或者delegate/block回调给VC来处理...至于这个 Avatar View 在 App 的各个地方都可能粗线,而且行为一致,那就意味着事件处理的 block,要散落在各个页面中,同时也带来了很多“只是为向上一层级转发事件”的 “Middle Man”这句话,我认为,除非block中的处理事件完全一致(都是加载同一个model,都是push/modal推出视图),否则做不到逻辑代码只有一份的情况,它还是得分散在各个VC中做对应的跳转...(个人观点,不喜勿喷)

POP的实现(Protocol)

如果说组合的缺点,调用时必须通过中间变量,管理它的创建和释放,多了一层构造(缺点是相对的,在POP之前都这样用..优点都是对比出来的)

typealias ClickButtonAction = () -> ()
class KQUserAvatarView: UIView {
    var btn: UIButton!
    var tapBlock: ClickButtonAction?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        btn = UIButton(type: .ContactAdd)
        btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
        self.addSubview(btn)
        
        btn.addTarget(self, action: "clickOnAvatarView", forControlEvents: .TouchUpInside)
    }
    
    func clickOnAvatarView() {
        if let blcok = tapBlock {
            blcok()
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

protocol UserAvatarViewAble: class {
    var userAvatarView: KQUserAvatarView! {get set}
    func setupUserAvatarView(tapHandle: ClickButtonAction?)
}

extension UserAvatarViewAble where Self: UIViewController {
    //  扩展不能实现储存属性
    func setupUserAvatarView(tapHandle: ClickButtonAction?) {
        userAvatarView = KQUserAvatarView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        userAvatarView.backgroundColor = UIColor.orangeColor()
        self.view.addSubview(userAvatarView)
        userAvatarView.tapBlock = tapHandle
    }
}

class ViewController: UIViewController, UserAvatarViewAble {
    var userAvatarView: KQUserAvatarView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUserAvatarView {
            print("点击了按钮")
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
  • 定义protocol,通过extension来实现协议,消除了中间变量
  • where Self: UIViewController用来规定只有采纳协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现.这个协议设计的就是作为VC的子视图控件,因此可以用UIViewController来直接限定,soself.view.addSubview(xxx)可以直接在协议的扩展中完成.
  • UIViewController属于类类型,因此协议UserAvatarViewAble必须用class关键字来修饰只能被类类型使用.还有就是在setupUserAvatarView方法中对属性变量进行了修改,如果是结构体/枚举采用了协议,必须用mutating关键字来修饰方法,否则就会报错...可以看看这个错误Protocol Extension, Mutating Function

这样看上去是不是很简洁易用?在Swift中很多场景都能通过它来实现.比如:检查手机号码,用户名的正则表达式判断..../颜色,图片的转换...等一系列的逻辑方法.
LZ之前objc项目中就存在各种:WCXxxUtil,WCXxxHandle,WCRegularUtil...
迁移到Swift中就类似Mixins and Traits in Swift 2.0写到的:

protocol ValidatesUsername {
    func isUsernameValid(password: String) -> Bool
}

extension ValidatesUsername {
    func isUsernameValid(username: String) -> Bool {
        if /* username too short */ {
            return false
        } else if /* username has invalid characters */ {
            return false
        } else {
            return true
        }
    }
}

class LoginViewController: UIViewController, ValidatesUsername, ValidatesPassword {
    @IBAction func loginButtonPressed() {
        if isUsernameValid(usernameTextField.text!) &&
            isPasswordValid(passwordTextField.text!) {
                // proceed with login
        } else {
            // show alert
        }
    }
}

protocol拆分了各种工具,extension实现默认设定,拿来即用,方便无污染.

POP在ViewModel中的体现
实现这样一个功能,写一个通讯录,要有头像和姓名-电话号码...
protocol层(不记得在哪里看到,对于协议的命令用形容词,果然IT最难的是命名...)

protocol PersonPresentAble {
    var nameTelText: String {get}
}

// 可以通过扩展提供默认实现...可用可不用
extension PersonPresentAble {
    var nameTelText: String {
        return "hehe"
    }
}

typealias TapImageViewAction = () -> ()
protocol ImagePresentAble {
    var showImage: UIImage? {get}
    var tapHandle: TapImageViewAction? {get}
}

ViewModel层

struct PersonModel {
    var firstName: String
    var lastName: String
    var fullName: String {
        return lastName + firstName
    }
    var telPhone: String
    var avatarImageUrl: String?
}

typealias TelPersonViewModelAble = protocol<PersonPresentAble,ImagePresentAble>
struct TelPersonViewModel: TelPersonViewModelAble {
    var telPerson: PersonModel
    var nameTelText: String
    var showImage: UIImage?
    var tapHandle: TapImageViewAction?
    
    init(model:PersonModel,tapHandle: TapImageViewAction?) {
        self.telPerson = model
        self.nameTelText = model.fullName + "  " + model.telPhone
        self.showImage = UIImage(named: model.avatarImageUrl!) // 暂时这样,按道理是加载url,否则没必要写到viewmodel中
        self.tapHandle = tapHandle
    }
}
  • fullName直接写成计算属性比较方便,当然你也可以在viewmodel中拼接
  • 保留一个model属性telPerson,因为有些赋值你不需要进行加工处理,比如年龄/身高
  • 虽然在PersonPresentAblenameTelText是get只读的,但是实现起来仍能可写.参见If a protocol requires a property to be gettable and settable, that property requirement cannot be fulfilled by a constant stored property or a read-only computed property. If the protocol only requires a property to be gettable, the requirement can be satisfied by any kind of property, and it is valid for the property to be also settable if this is useful for your own code.

View层和ViewController层

class ContactTableViewCell: UITableViewCell {
    @IBOutlet weak var telTextLabel: UILabel!
    @IBOutlet weak var avatarImageView: UIImageView!
    
    var tapHandle: TapImageViewAction?

    override func awakeFromNib() {
        super.awakeFromNib()
        let tapGesture = UITapGestureRecognizer(target: self, action: "tapAction")
        avatarImageView.addGestureRecognizer(tapGesture)
    }
    
    func configureDataWithViewModel(viewModel: TelPersonViewModelAble) {
        telTextLabel.text = viewModel.nameTelText
        avatarImageView.image = viewModel.showImage
        tapHandle = viewModel.tapHandle
    }
    
    func tapAction() {
        if let block = tapHandle {
            block()
        }
    }
}

// VC
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("hehe", forIndexPath: indexPath) as! ContactTableViewCell
    let testModel = PersonModel(firstName: "明涛", lastName: "胡", telPhone: "15279107716", avatarImageUrl: "麒麟星.jpg")
    let testViewModel = TelPersonViewModel(model: testModel) {
        print("我点击了头像")
    }
    cell.configureDataWithViewModel(testViewModel)
    return cell
}
  • 通过合成协议typealias TelPersonViewModelAble = protocol<PersonPresentAble,ImagePresentAble>来定义viewmodel的类型,代码复用性高
  • 在objc中,viewmodel的类型常常容易被定死,存在共同属性的时候又走上了继承的老路了...比如:

    这时只需要另定义个protocol,无须写父类弄继承,依旧那句话,让写功能跟搭积木一样:
protocol CompanyPresentAble {
    var positionText: String {get}
}

typealias InvestPersonViewModelAble = protocol<PersonPresentAble,ImagePresentAble,CompanyPresentAble>
...剩下的,你懂怎么写的^_^

参考资料:
Mixins 比继承更好
Swift中的协议编程
Introducing Protocol-Oriented Programming in Swift 2
Updated: Protocol-Oriented MVVM in Swift 2.0
Mixins and Traits in Swift 2.0
iOS应用架构谈 view层的组织和调用方案

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

推荐阅读更多精彩内容