引
记得从大学学编程开始,对于软件编程听的最多的就是面向对象编程(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),这一点可以理解为封装性.而继承和多态,是struct
和enum
不具备的,它则是通过遵守protocol
来实现.
但,这些是为了说明让我们放弃OOP
吗?这是不可能的....想想UIKit
....记得刚开始用Swift写项目时,总是告诫自己,不能只是机械的把objc 翻译成 swift
.实际开发项目中,ViewControl
和View
基本都是使用系统的框架,通过继承来实现,无论如何的自定义,都是要围绕苹果的那一套来,OC与swift在这一块保持一致;但在model
和handle/viewModel/manager
这一块,更多的通过POP实现,后面会通过一个例子来说明.(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
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
,因为有些赋值你不需要进行加工处理,比如年龄/身高 - 虽然在
PersonPresentAble
中nameTelText
是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
来定义viewmodel的类型,代码复用性高 - 在objc中,viewmodel的类型常常容易被定死,存在共同属性的时候又走上了继承的老路了...比如:
这时只需要另定义个protocol,无须写父类弄继承,依旧那句话,让写功能跟搭积木一样:
protocol CompanyPresentAble {
var positionText: String {get}
}
typealias InvestPersonViewModelAble = protocol
...剩下的,你懂怎么写的^_^
参考资料:
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层的组织和调用方案