引
記得從大學學編程開始,對于軟件編程聽的最多的就是面向?qū)ο缶幊?Object Oriented Programming匕得,OOP)
了,它的三大特征:封裝,繼承,多態(tài).而Swift倡導的面向協(xié)議編程(Protocol-oriented programming,POP)
是OOP的一個范例,我理解為"封裝+協(xié)議*結(jié)構(gòu)體+擴展"(Swift2.0開始,你可以擴展一個protocol
)
WWDC:Protocol-Oriented Programming in Swift開頭Classess Are Awesome
,指出OOP中的Classes
提供了:數(shù)據(jù)的封裝、訪問控制、抽象化运翼、命名空間等,但這些都是Classes
才特有的屬性嗎?事實上,這些都是類型(Type)
的所有屬性,Classes
只是Type
的一種實現(xiàn)方法,在Swift中I can do all that with structs and enums
(Swift的標準庫組成:55個Protocols和102個Structs),這一點可以理解為封裝性.而繼承和多態(tài),是struct
和enum
不具備的,它則是通過遵守protocol
來實現(xiàn).
但,這些是為了說明讓我們放棄OOP
嗎?這是不可能的....想想UIKit
....記得剛開始用Swift寫項目時,總是告誡自己,不能只是機械的把objc 翻譯成 swift
.<font color="IndianRed">實際開發(fā)項目中,ViewControl
和View
基本都是使用系統(tǒng)的框架,通過繼承來實現(xiàn),無論如何的自定義,都是要圍繞蘋果的那一套來,OC與swift在這一塊保持一致;但在model
和handle/viewModel/manager
這一塊,更多的通過POP實現(xiàn),后面會通過一個例子來說明.</font>(PS:在oc中,我們體會的是OOP/FRP(參考一下RAC
),那Swift就是OOP/FRP(參考一下RxSwift
)/POP;在oc中對于Protocols的理解更多的是UIAppplicationDelegate,UITableViewDelegete,NSCopying,UITextFieldDelegate....
,而Swift中Protocols則被賦予了更多的功能和意義:"可定義屬性,可組合,可繼承,可擴展,支持泛型,支持類/結(jié)構(gòu)體/枚舉").在Swift面向協(xié)議編程初探中,bz總結(jié)的一句話,非常nice:
面向?qū)ο缶幊毯兔嫦騾f(xié)議編程最明顯的區(qū)別在于程序設(shè)計過程中對數(shù)據(jù)類型的抽取(抽象)上,面向?qū)ο缶幊淌褂妙惡屠^承的手段灶泵,數(shù)據(jù)類型是引用類型
面向協(xié)議編程使用的是遵守協(xié)議的手段货矮,數(shù)據(jù)類型是值類型(Swift中的結(jié)構(gòu)體或枚舉)
PS:值類型
和引用類型
的區(qū)別這里不作詳敘,可參考Swift:什么時候使用結(jié)構(gòu)體和類
討厭的"上帝"(Inheritence)
繼承
帶給我們最大的問題,可能就是常常會構(gòu)造出所謂的God類/super類
,帶來的壞處也隨之可見:
- 一層一層一層的傳遞下去,它的任何行為都會影響它的所有小弟;
- 有的小弟繼承了無用的屬性和方法;
- 不方便擴展,差別不大的同類上帝,直接拷貝一遍代碼?
特別喜歡田偉宇博客:跳出面向?qū)ο笏枷?一) 繼承中提到關(guān)于繼承
的要點之一:父類的所有變化炫彩,都需要在子類中體現(xiàn)匾七,也就是說此時耦合已經(jīng)成為需求.(他的文章非常nice,在架構(gòu)這一塊的寫的系列文章值得深讀)so,LZ的觀點也是萬不得已不要用繼承,優(yōu)先考慮組合
!
注:在objc中,更多的是用組合(Composition)
,在Swift中則是協(xié)議>組合>繼承
.后面會舉例說明.
再注:全文的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")
}
}
// 假設(shè)超人會飛不會游泳,復制飛的方法
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都不存在多繼承,會游泳的超人,這時要復制游的方法,到這里已經(jīng)是第四層了...高耦合
- 也不好直接把fly()定義到父類Animal中,等于強加限制.因為通過繼承,抽象出共同的性質(zhì),Bird/Preson/Fish都是動物(人是高級動物),它們都有屬性name和type,都具有eat()的行為,但fly()不是所有動物共有的
- 這時來了一個外星生物,它不屬于Animal,但是擁有Animal及其子類所有的屬性和行為(方法),怎么辦?
上帝類
都幫不了你了,又走上了重復復制之路!
有句話是咋說的:我們區(qū)分鳥和魚,不是因為它們的名字是鳥/魚,而是通過它們表現(xiàn)的行為,有點亂,_.把所有的行為拆分出來,通過搭積木的形式組合出來,你具備什么就拿什么,那么你的身份也就隨之浮現(xiàn)了.
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 "超水人"http:// 好蠢的名字...
}
func fly() {
print("...")
}
func speak() {
print("...")
}
func swimming() {
print("....")
}
}
- objc中,還是可以定義一個父類Animal的,LZ現(xiàn)在基本都是寫Swift了,就直接定義了一個protocol:
Property
,在擴展中寫好默認實現(xiàn) - 消滅了
上帝類
,全部都定義為protocol,用到什么就拼接什么,真的就夠搭積木一樣便捷...
組合(Composition),哎喲不錯
第一個例子屬于對于model
定義,接下來看一個view
層所表現(xiàn)出的問題.
手機QQ底部tabbar的三個標簽首頁都帶有一個頭像控件,最開始我們采取繼承的形式來實現(xiàn)一個baseVC
class KQUserAvatarView: UIView {
}
class KQBaseViewController: UIViewController {
var userAvatarView: KQUserAvatarView!
func setupUserAvatarView() {
}
func clickOnAvatarView() {
}
}
- 新需求,希望第一、第二個標簽頁的頭像加上大V的標志,第三頁保持不變,此刻高耦合,父類改動牽動三個子類/甚至更多子類的變化.或許你直接在父類中添加改變樣式的方法,那么那些不需要改變的子類也就直接繼承了無用的方法...
- 又來個需求,我需要一個父類是
UITableViewController
的新KQUserAvatarView
,瞬間傻眼...只能復制代碼再創(chuàng)造一個上帝了 - 這種情況還只是一個view的創(chuàng)建,如果是好幾個組合view的組成,那么VC中的代碼簡直就是災(zāi)難...
在objc/Swift1.2之前的,我們用組合來代替繼承
,這是非常常見的一種做法.借助中間件,解耦+轉(zhuǎn)移邏輯代碼,減輕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.
}
}
- 哪個頁面需要頭像控件,直接創(chuàng)建一個
KQUserAvatarViewManager
對象進行引用就行,實現(xiàn)了解耦 - 多個view對應(yīng)對個manager,很好的給VC進行了瘦身
- 題外話:前端時間看了陽神的iOS 開發(fā)中的 Self-Manager 模式,評論也看了...對于文章所訴的觀點,LZ我也是贊同評論中提議的創(chuàng)建一個
ViewManager
,在它里面處理點擊事件或者delegate/block回調(diào)給VC來處理...至于這個 Avatar View 在 App 的各個地方都可能粗線江兢,而且行為一致昨忆,那就意味著事件處理的 block,要散落在各個頁面中杉允,同時也帶來了很多“只是為向上一層級轉(zhuǎn)發(fā)事件”的 “Middle Man”
這句話,我認為,除非block中的處理事件完全一致(都是加載同一個model,都是push/modal推出視圖),否則做不到邏輯代碼只有一份的情況,它還是得分散在各個VC中做對應(yīng)的跳轉(zhuǎn)...(個人觀點,不喜勿噴)
POP的實現(xiàn)(Protocol)
如果說組合的缺點,調(diào)用時必須通過中間變量,管理它的創(chuàng)建和釋放,多了一層構(gòu)造(缺點是相對的,在POP之前都這樣用..優(yōu)點都是對比出來的)
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 {
// 擴展不能實現(xiàn)儲存屬性
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來實現(xiàn)協(xié)議,消除了中間變量
-
where Self: UIViewController
用來規(guī)定只有采納協(xié)議的類型滿足這些限制條件時邑贴,才能獲得協(xié)議擴展提供的默認實現(xiàn)
.這個協(xié)議設(shè)計的就是作為VC的子視圖控件,因此可以用UIViewController
來直接限定,soself.view.addSubview(xxx)
可以直接在協(xié)議的擴展中完成. -
UIViewController
屬于類類型,因此協(xié)議UserAvatarViewAble
必須用class
關(guān)鍵字來修飾只能被類類型使用.還有就是在setupUserAvatarView
方法中對屬性變量進行了修改,如果是結(jié)構(gòu)體/枚舉采用了協(xié)議,必須用mutating
關(guān)鍵字來修飾方法,否則就會報錯...可以看看這個錯誤Protocol Extension, Mutating Function
這樣看上去是不是很簡潔易用?在Swift中很多場景都能通過它來實現(xiàn).比如:檢查手機號碼,用戶名的正則表達式判斷..../顏色,圖片的轉(zhuǎn)換...等一系列的邏輯方法.
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實現(xiàn)默認設(shè)定,拿來即用,方便無污染
.
POP在ViewModel中的體現(xiàn)
實現(xiàn)這樣一個功能,寫一個通訊錄,要有頭像和姓名-電話號碼...
protocol層(不記得在哪里看到,對于協(xié)議的命令用形容詞,果然IT最難的是命名...)
protocol PersonPresentAble {
var nameTelText: String {get}
}
// 可以通過擴展提供默認實現(xiàn)...可用可不用
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
,因為有些賦值你不需要進行加工處理,比如年齡/身高 - 雖然在
PersonPresentAble
中nameTelText
是get只讀的,但是實現(xiàn)起來仍能可寫.參見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
}
- 通過合成協(xié)議
typealias TelPersonViewModelAble = protocol<PersonPresentAble,ImagePresentAble>
來定義viewmodel的類型,代碼復用性高 - 在objc中,viewmodel的類型常常容易被定死,存在共同屬性的時候又走上了繼承的老路了...比如:
這時只需要另定義個protocol,無須寫父類弄繼承,依舊那句話,讓寫功能跟搭積木一樣:
protocol CompanyPresentAble {
var positionText: String {get}
}
typealias InvestPersonViewModelAble = protocol<PersonPresentAble,ImagePresentAble,CompanyPresentAble>
...剩下的,你懂怎么寫的^_^
參考資料:
Mixins 比繼承更好
Swift中的協(xié)議編程
Introducing Protocol-Oriented Programming in Swift 2
Updated: Protocol-Oriented MVVM in Swift 2.0
Mixins and Traits in Swift 2.0
iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案