此文為資料匯總文鹊汛,基于自己的理解收集網(wǎng)絡(luò)上簡(jiǎn)明易懂的文章及例子硼端,通篇瀏覽之后會(huì)對(duì)這個(gè)概念會(huì)有初步的認(rèn)識(shí)。
參考資料:面向“接口”編程和面向“實(shí)現(xiàn)”編程
Protocol-Oriented Programming in Swift
Introducing Protocol-Oriented Programming in Swift 2
IF YOU'RE SUBCLASSING, YOU'RE DOING IT WRONG.
and so on...
因?yàn)楹?jiǎn)書(shū)的Markdown 不支持 [toc]生成目錄给梅,另外一種方式生成目錄較為繁瑣假丧,所以貼個(gè)圖片版的目錄,另外希望簡(jiǎn)書(shū)早點(diǎn)支持[toc]:
什么叫做面向協(xié)議編程
我自己感覺(jué)比較明確的定義就是Apple wwdc15/408視頻中的那句話:
Don't start with a class.
Start with a protocol.
從協(xié)議的角度開(kāi)始編程??
談?wù)劽鎸?duì)對(duì)象編程(OOP)的一些弊端
如圖如何走心动羽。
面對(duì)對(duì)象的目的是大規(guī)模重用代碼包帚。
面對(duì)對(duì)象的手段是綁定結(jié)構(gòu)和函數(shù)。
面對(duì)對(duì)對(duì)象的哲學(xué)含義是形象化抽象一個(gè)虛擬物體运吓。
以上三個(gè)點(diǎn)可謂是面對(duì)對(duì)象編程的定義以及面對(duì)對(duì)象的好處渴邦,一旦聊到面對(duì)對(duì)象總會(huì)伴隨 “代碼重用”。我們從真實(shí)的世界來(lái)考慮這個(gè)問(wèn)題拘哨,我們對(duì)客觀存在的主體看法是會(huì)隨著時(shí)間的改變而改變的谋梭,真實(shí)世界中甚至不存在固定形式化的抽象,而代碼是為了具體問(wèn)題而出現(xiàn)的倦青,所以不存在通用抽象瓮床,也就不存在可以無(wú)限重用的定義和邏輯。所以對(duì)象也就是用于計(jì)算的模型而已产镐,技術(shù)手段是正確的(給數(shù)據(jù)綁定操作) 但是對(duì)于目標(biāo)(大規(guī)模代碼重用)相去甚遠(yuǎn)隘庄,能重用的應(yīng)該只有為了解決問(wèn)題的方法,而不是只有模型癣亚。另外的難點(diǎn)丑掺,不同人為了解決相似問(wèn)題,開(kāi)發(fā)出來(lái)的模型可以十萬(wàn)八千里述雾,為了重用模型街州,修改之后又能適應(yīng)新問(wèn)題,于是這叫泛化玻孟,它估計(jì)你去制造全能模型唆缴,但是不僅難,還沒(méi)法向后兼容取募,有時(shí)候就硬是要把飛機(jī)做成魚(yú)……這就是面向?qū)ο笏季S的硬傷琐谤,創(chuàng)造出一個(gè)大家都懂蟆技,大家都認(rèn)為對(duì)玩敏,大家都能拿去用的模型太難6芳伞(摘自知乎精選)
我自己的感覺(jué),類的繼承讓代碼的可讀性大大降低旺聚,比如我想知道這個(gè)類用途還要去看這個(gè)類的父類能干嘛假如它還有個(gè)祖父類呢织阳?而且想想看假如一個(gè)項(xiàng)目由一個(gè)基類開(kāi)始,并伴生了很多子類砰粹,解決需求發(fā)現(xiàn)需要更改基類的時(shí)候唧躲,不敢動(dòng)手是多么恐怖的一件事情。
Java程序員對(duì)單個(gè)方法的實(shí)現(xiàn)超過(guò)10行感到非常不安碱璃,這代表自己的代碼可重用性很差弄痹。于是他把一個(gè)3個(gè)參數(shù)的長(zhǎng)方法拆成了4個(gè)子過(guò)程,每個(gè)子過(guò)程有10個(gè)以上的參數(shù)嵌器。后來(lái)他覺(jué)得這樣很不OOP肛真,于是他又創(chuàng)建了4個(gè)interface和4個(gè)class。
由一個(gè)簡(jiǎn)單的例子開(kāi)始
讓我們由這個(gè)例子開(kāi)始面向“協(xié)議”編程
例子采用Rust語(yǔ)言爽航,編輯器推薦使用CodeRunner
先用面對(duì)對(duì)象的視角蚓让,書(shū)可以燃燒,于是書(shū)有個(gè)方法 burn()讥珍。
書(shū)并不是唯一會(huì)燃燒的東西历极,木頭也可以燃燒,它也有一個(gè)方法叫做 burn()衷佃√诵叮看看不是面向“協(xié)議”下是如何燃燒:
struct Book {
title: @str,
author: @str,
}
struct Log {
wood_type: @str,
}
這兩個(gè)結(jié)構(gòu)體分別表示書(shū)(Book)和木頭(Log),下面實(shí)現(xiàn)它們的方法:
impl Log {
fn burn(&self) {
println(fmt!("The %s log is burning!", self.wood_type));
}
}
impl Book {
fn burn(&self) {
println(fmt!("The book %s by %s is burning!", self.title, self.author));
}
}
現(xiàn)在書(shū)與木頭都有了 burn() 方法纲酗,現(xiàn)在我們燒它們衰腌。
先放木頭:
fn start_fire(lg: Log) {
lg.burn();
}
fn main() {
let lg = Log {
wood_type: @"Oak",
length: 1,
};
// Burn the oak log!
start_fire(lg);
}
一切ok,輸出 "The Oak log is burning!"觅赊。
現(xiàn)在因?yàn)槲覀円呀?jīng)有了一個(gè) start_fire 函數(shù)右蕊,是否我們可以把書(shū)也傳進(jìn)去,因?yàn)樗鼈兌加?burn():
fn main() {
let book = Book {
title: @"The Brothers Karamazov",
author: @"Fyodor Dostoevsky",
};
// Let's try to burn the book...
start_fire(book);
}
可行么吮螺?肯定不行叭那簟!函數(shù)已經(jīng)指名需要Log結(jié)構(gòu)體鸠补,而不是Book結(jié)構(gòu)體萝风,怎么解決這個(gè)問(wèn)題,再寫(xiě)一個(gè)函數(shù)接受Book結(jié)構(gòu)體紫岩?這樣只會(huì)得到兩個(gè)幾乎一樣的函數(shù)规惰。
解決這個(gè)問(wèn)題
加一個(gè)協(xié)議接口,協(xié)議接口在Rust語(yǔ)言中叫做 trait :
struct Book {
title: @str,
author: @str,
}
struct Log {
wood_type: @str,
}
trait Burnable {
fn burn(&self);
}
多了一個(gè) Burnable 的接口泉蝌,為每個(gè)結(jié)構(gòu)體實(shí)現(xiàn)它們的接口:
impl Burnable for Log {
fn burn(&self) {
println(fmt!("The %s log is burning!", self.wood_type));
}
}
impl Burnable for Book {
fn burn(&self) {
println(fmt!("The book \"%s\" by %s is burning!", self.title, self.author));
}
}
接下來(lái)實(shí)現(xiàn)點(diǎn)火函數(shù)
fn start_fire<T: Burnable>(item: T) {
item.burn();
}
這里Swift跟Rust很像歇万,T 占位符表示任何實(shí)現(xiàn)了這個(gè)接口的類型揩晴。
這樣我們只要往函數(shù)里面?zhèn)魅我鈱?shí)現(xiàn)了 Burnable 協(xié)議接口的類型就沒(méi)有問(wèn)題。主函數(shù):
fn main() {
let lg = Log {
wood_type: @"Oak",
};
let book = Book {
title: @"The Brothers Karamazov",
author: @"Fyodor Dostoevsky",
};
// Burn the oak log!
start_fire(lg);
// Burn the book!
start_fire(book);
}
成功輸出:
The Oak log is burning!
The book “The Brothers Karamazov” by Fyodor Dostoevsky is burning!
于是這個(gè)函數(shù)完全能復(fù)用任意實(shí)現(xiàn)了 Burnable 協(xié)議接口的實(shí)例贪磺,cool...
在Objective-C中如何面對(duì)協(xié)議編程
OC畢竟是以面向?qū)ο鬄樵O(shè)計(jì)基礎(chǔ)的硫兰,所以實(shí)現(xiàn)比較麻煩,接口在OC中為Protocol寒锚,Swift中強(qiáng)化了Protocol協(xié)議的地位(下節(jié)再講Swift中的面向協(xié)議)劫映。
目前大部分開(kāi)發(fā)以面向?qū)ο缶幊虨橹鳎热缡褂?ASIHttpRequest 來(lái)執(zhí)行網(wǎng)絡(luò)請(qǐng)求:
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDidFinishSelector:@selector(requestDone:)];
[request setDidFailSelector:@selector(requestWrong:)];
[request startAsynchronous];
發(fā)起請(qǐng)求的時(shí)候刹前,我們需要知道要給request對(duì)象賦值哪一些屬性并調(diào)用哪一些方法泳赋,現(xiàn)在來(lái)看看 AFNetworking 的請(qǐng)求方式:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:@"www.olinone.com" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"good job");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//to do
}];
一目了然,調(diào)用者不用關(guān)心它有哪些屬性喇喉,除非接口無(wú)法滿足需求需要去了解相關(guān)屬性的定義摹蘑。這是兩種完全不同的設(shè)計(jì)思路。
接口比屬性直觀
定義一個(gè)對(duì)象的時(shí)候轧飞,一般都要為它定義一些屬性衅鹿,比如 ReactiveCocoa 中的 RACSubscriber 對(duì)象定義:
@interface RACSubscriber ()
@property (nonatomic, copy) void (^next)(id value);
@property (nonatomic, copy) void (^error)(NSError *error);
@property (nonatomic, copy) void (^completed)(void);
@end
以接口的形式提供入口:
@interface RACSubscriber
+ (instancetype)subscriberWithNext:(void (^)(id x))next
error:(void (^)(NSError *error))error
completed:(void (^)(void))completed;
@end
接口比屬性更加直觀,抽象的接口直接描述要做的事情过咬。
接口依賴
設(shè)計(jì)一個(gè)APIService對(duì)象
@interface ApiService : NSObject
@property (nonatomic, strong) NSURL *url;
@property (nonatomic, strong) NSDictionary *param;
- (void)execNetRequest;
@end
正常發(fā)起Service請(qǐng)求時(shí)大渤,調(diào)用者需要直接依賴該對(duì)象,起不到解耦的目的掸绞。當(dāng)業(yè)務(wù)變動(dòng)需要重構(gòu)該對(duì)象時(shí)泵三,所有引用該對(duì)象的地方都需要改動(dòng)。如何做到既能滿足業(yè)務(wù)又能兼容變化衔掸?抽象接口也許是一個(gè)不錯(cuò)的選擇烫幕,以接口依賴的方式取代對(duì)象依賴,改造代碼如下:
@protocol ApiServiceProtocol
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param;
@end
@interface NSObject (ApiServiceProtocol) <ApiServiceProtocol>
@end
@implementation NSObject (ApiServiceProtocol)
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
ApiService *apiSrevice = [ApiService new];
apiSrevice.url = url;
apiSrevice.param = param;
[apiSrevice execNetRequest];
}
@end
通過(guò)接口的定義敞映,調(diào)用者可以不再關(guān)心ApiService對(duì)象较曼,也無(wú)需了解其有哪些屬性。即使需要重構(gòu)替換新的對(duì)象振愿,調(diào)用邏輯也不受任何影響捷犹。調(diào)用接口往往比訪問(wèn)對(duì)象屬性更加穩(wěn)定可靠。
抽象對(duì)象
定義ApiServiceProtocol可以隱藏ApiService對(duì)象冕末,但是受限于ApiService對(duì)象的存在萍歉,業(yè)務(wù)需求發(fā)生變化時(shí),仍然需要修改ApiService邏輯代碼档桃。如何實(shí)現(xiàn)在不修改已有ApiService業(yè)務(wù)代碼的條件下滿足新的業(yè)務(wù)需求枪孩?
參考Swift抽象協(xié)議的設(shè)計(jì)理念,可以使用Protocol抽象對(duì)象,畢竟調(diào)用者也不關(guān)心具體實(shí)現(xiàn)類蔑舞。Protocol可以定義方法丛晌,可是屬性的問(wèn)題怎么解決?此時(shí)斗幼,裝飾器模式也許正好可以解決該問(wèn)題,讓我們?cè)囍^續(xù)改造ApiService
@protocol ApiService <ApiServiceProtocol>
// private functions
@end
@interface ApiServicePassthrough : NSObject
@property (nonatomic, strong) NSURL *url;
@property (nonatomic, strong) NSDictionary *param;
- (instancetype)initWithApiService:(id<ApiService>)apiService;
- (void)execNetRequest;
@end
@interface ApiServicePassthrough ()
@property (nonatomic, strong) id<ApiService> apiService;
@end
@implementation ApiServicePassthrough
- (instancetype)initWithApiService:(id<ApiService>)apiService {
if (self = [super init]) {
self.apiService = apiService;
}
return self;
}
- (void)execNetRequest {
[self.apiService requestNetWithUrl:self.url Param:self.param];
}
@end
經(jīng)過(guò)Protocol的改造抚垄,ApiService對(duì)象化身為ApiService接口蜕窿,其不再依賴于任何對(duì)象,做到了真正的接口依賴取代對(duì)象依賴呆馁,具有更強(qiáng)的業(yè)務(wù)兼容性
定義一個(gè)Get請(qǐng)求對(duì)象
@interface GetApiService : NSObject <ApiService>
@end
@implementation GetApiService
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
// to do
}
@end
改變請(qǐng)求代碼
@implementation NSObject (ApiServiceProtocol)
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
id<ApiService> apiSrevice = [GetApiService new];
ApiServicePassthrough *apiServicePassthrough = [[ApiServicePassthrough alloc] initWithApiService:apiSrevice];
apiServicePassthrough.url = url;
apiServicePassthrough.param = param;
[apiServicePassthrough execNetRequest];
}
@end
對(duì)象可以繼承對(duì)象桐经,Protocol也可以繼承Protocol,并且可以繼承多個(gè)Protocol浙滤,Protocol具有更強(qiáng)的靈活性阴挣。某一天,業(yè)務(wù)需求變更需要用到新的Post請(qǐng)求時(shí)纺腊,可以不用修改 GetApiService一行代碼畔咧,定義一個(gè)新的 PostApiService實(shí)現(xiàn)Post請(qǐng)求即可,避免了對(duì)象里面出現(xiàn)過(guò)多的if-else代碼揖膜,也保證了代碼的整潔性誓沸。
依賴注入
GetApiService依然是以對(duì)象依賴的形式存在,如何解決這個(gè)問(wèn)題壹粟?沒(méi)錯(cuò)拜隧,依賴注入!依賴注入會(huì)讓測(cè)試變得可行趁仙。
關(guān)于依賴注入可以看這篇文章
借助 objection 開(kāi)源庫(kù)改造ApiService:
@implementation NSObject (ApiServiceProtocol)
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
id<ApiService> apiSrevice = [[JSObjection createInjector] getObject:[GetApiService class]];
ApiServicePassthrough *apiServicePassthrough = [[ApiServicePassthrough alloc] initWithApiService:apiSrevice];
apiServicePassthrough.url = url;
apiServicePassthrough.param = param;
[apiServicePassthrough execNetRequest];
}
@end
調(diào)用者關(guān)心請(qǐng)求接口洪添,實(shí)現(xiàn)者關(guān)心需要實(shí)現(xiàn)的接口,各司其職雀费,互不干涉干奢。
我自己的感覺(jué),利用OC來(lái)進(jìn)行面向協(xié)議編程還是繞不過(guò)類這個(gè)坎盏袄,反而變成為了面向協(xié)議編程而進(jìn)行協(xié)議編程律胀,比較捉雞。
Swift中的面向協(xié)議編程
WWDC15上表示
Swift is a Protocol-Oriented Programming Language
Talk is cheap貌矿,show you the code!
先看看這個(gè)例子
class Ordered {
func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}
class Number : Ordered {
var value: Double = 0
override func precedes(other: Ordered) -> Bool {
return value < (other as! Number).value
}
}
as炭菌!在swift中表示強(qiáng)制類型轉(zhuǎn)換。
對(duì)于這種情況逛漫,蘋(píng)果的工程師表示這是一種 Lost Type Relationships黑低,我對(duì)這個(gè)的理解 是 失去對(duì)類型的控制。也就是這個(gè)Number類的函數(shù)往里邊傳非Number類型的參數(shù)會(huì)出問(wèn)題】宋眨可能你們覺(jué)得這個(gè)問(wèn)題還好蕾管,只要注意下Number下函數(shù)的函數(shù)實(shí)現(xiàn)就好了,但是在大型項(xiàng)目中菩暗,你使用一個(gè)類因?yàn)閾?dān)心類型問(wèn)題而需要去看類的實(shí)現(xiàn)掰曾,這樣的編碼是不是很讓人煩躁?
利用Protocol來(lái)重寫(xiě)
直接上代碼吧:
protocol Ordered {
func precedes(other: Self) -> Bool
}
struct Number : Ordered {
var value: Double = 0
func precedes(other: Number) -> Bool {
return self.value < other.value
}
}
用swift中的struct(結(jié)構(gòu)體)來(lái)取代class
protocol 的Self表示任何遵循了這個(gè)協(xié)議的類型⊥M牛現(xiàn)在就不用擔(dān)心類型的問(wèn)題了旷坦。
struct與class的區(qū)別
struct是值拷貝類型,而class是引用類型佑稠。這也是apple的工程師推薦使用struct代替class的原因秒梅。
struct無(wú)法繼承,class可以繼承舌胶。
關(guān)于值拷貝與引用的區(qū)別看下面的
code:
struct Dog{
var owner : String?
}
var 梅西的狗 = Dog(owner:"梅西")
var C羅的狗 = Dog(owner:"C羅")
var 貝爾的狗 = Dog(owner:"貝爾")
print(梅西的狗.owner,"與",C羅的狗.owner)
//此行輸出 梅西與C羅
C羅的狗 = 梅西的狗
print(梅西的狗.owner,"與",C羅的狗.owner)
//此行輸出 梅西與梅西
梅西的狗 = 貝爾的狗
print(梅西的狗.owner,"與",C羅的狗.owner)
//此行輸出 貝爾與梅西
//C羅的狗.owner還是梅西
//使用class
class DogClass{
var owner : String?
}
var 梅西的狗 = DogClass()
梅西的狗.owner = "梅西"
var C羅的狗 = DogClass()
C羅的狗.owner = "C羅"
var 貝爾的狗 = DogClass()
貝爾.owner = "貝爾"
print(梅西的狗.owner,"與",C羅的狗.owner)
//此行輸出 梅西與C羅
C羅的狗 = 梅西的狗
print(C羅的狗.owner)
//此行輸出 梅西
梅西的狗.owner = 貝爾的狗.owner
print(梅西的狗.owner,"與",C羅的狗)
//此行輸出 貝爾與貝爾
// C羅的狗的owner也變?yōu)樨悹柫?
再插入一幅圖來(lái)理解引用類型吧:
簡(jiǎn)單的運(yùn)用下我們定義的這個(gè)協(xié)議吧
以下是一個(gè)簡(jiǎn)單的二分查找算法函數(shù)實(shí)現(xiàn):
func binarySearch<T : Ordered>(sortedKeys: [T], forKey k: T) -> Int {
var lo = 0
var hi = sortedKeys.count
while hi > lo {
let mid = lo + (hi - lo) / 2
if sortedKeys[mid].precedes(k) { lo = mid + 1 }
else { hi = mid }
}
return lo }
其中T(可以理解為 “占位符”)表示任何遵循 Ordered 協(xié)議的類型捆蜀,這里就和開(kāi)頭使用Rust語(yǔ)言實(shí)現(xiàn)的程序異曲同工了。
Swift2.0引入的一個(gè)重要特性 protocol extension
也就是我們可以擴(kuò)展協(xié)議幔嫂,cool辆它。
我們可以定義一個(gè)協(xié)議:
protocol MyProtocol {
func method()
}
然后在這個(gè)協(xié)議的extension中增加函數(shù) method() 的實(shí)現(xiàn):
extension MyProtocol {
func method() {
print("Called")
}
}
創(chuàng)建一個(gè) struct 遵循這個(gè)協(xié)議:
struct MyStruct: MyProtocol {
}
MyStruct().method()
// 輸出:
// Called
這樣就可以實(shí)現(xiàn)類似繼承的功能,而不需要成為某個(gè)類的子類履恩。
cool嗎娩井?現(xiàn)在我們回過(guò)頭來(lái)想想,使用OC編程中似袁,系統(tǒng)固有的協(xié)議不借助黑魔法我們是否可以對(duì)已有的協(xié)議進(jìn)行擴(kuò)展洞辣?不能!(關(guān)于在OC中如何擴(kuò)展協(xié)議自行搜索昙衅,此處不展開(kāi)了)扬霜。
一個(gè)簡(jiǎn)單的例子運(yùn)用 protocol extension
定義一個(gè) Animal 協(xié)議和動(dòng)物的屬性:
protocol Animal {
var name: String { get }
var canFly: Bool { get }
var canSwim: Bool { get }
}
定義三個(gè)具體的動(dòng)物:
struct Parrot: Animal {
let name: String
let canFly = true
let canSwim = false
}
struct Penguin: Animal {
let name: String
let canFly = true
let canSwim = true
}
struct Goldfish: Animal {
let name: String
let canFly = false
let canSwim = true
}
每一個(gè)動(dòng)物都要實(shí)現(xiàn)一遍它們的 canFly 與 canSwim 屬性顯得很業(yè)余。
現(xiàn)在來(lái)定義Flyable而涉、Swimable兩個(gè)Protocol:
protocol Flyable {
}
protocol Swimable {
}
利用 extension給protocol添加默認(rèn)實(shí)現(xiàn):
extension Animal {
var canFly: Bool { return false }
var canSwim: Bool { return false }
}
extension Animal where Self: Flyable {
var canFly: Bool { return true }
}
extension Animal where Self: Swimable {
var canSwim: Bool { return true }
}
這樣符合Flyable協(xié)議的Animal著瓶,canFly屬性為true,復(fù)合Swimable的Animal啼县,canSwim屬性為true材原。
改造上面三個(gè)結(jié)構(gòu)體:
struct Parrot: Animal, Flyable {
let name: String
}
struct Penguin: Animal, Flyable, Swimable {
let name: String
}
struct Goldfish: Animal, Swimable {
let name: String
}
在將來(lái),你需要改動(dòng)代碼季眷,比如 Parrot 老了余蟹,沒(méi)辦法飛了,就將Flyable的協(xié)議去掉即可子刮。
好處:
- class只能繼承一個(gè)class威酒,類型可以遵循多個(gè)protocol窑睁,就可以同時(shí)被多個(gè)protocol實(shí)現(xiàn)多個(gè)默認(rèn)行為。
- class葵孤,struct担钮,enum都可以遵循protocol,而class的繼承只能是class尤仍,protocol能給值類型提供默認(rèn)的行為箫津。
- 高度解耦不會(huì)給類型引進(jìn)額外的狀態(tài)。
一個(gè)簡(jiǎn)單的實(shí)戰(zhàn)
這樣一個(gè)簡(jiǎn)單的需求宰啦,一個(gè)登陸頁(yè)面苏遥,用戶輸入的密碼錯(cuò)誤之后,密碼框會(huì)有一個(gè)抖動(dòng)绑莺,實(shí)現(xiàn)起來(lái)很簡(jiǎn)單:
import UIKit
class FoodImageView: UIImageView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
好了,現(xiàn)在產(chǎn)品告訴你惕耕,除了密碼框要抖動(dòng)纺裁,登陸按鈕也要抖動(dòng),那這樣:
import UIKit
class ActionButton: UIButton {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
這已然是兩個(gè)重復(fù)的代碼司澎,而且當(dāng)你需要變動(dòng)動(dòng)畫(huà)時(shí)候欺缘,你需要改動(dòng)兩處的代碼,這很不ok挤安,有OC編程經(jīng)驗(yàn)的人會(huì)想到利用Category的方式谚殊,在swift中即為extension,改造如下:
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: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
這樣看起來(lái)似乎已經(jīng)很棒了蛤铜,因?yàn)槲覀児?jié)約了代碼嫩絮,但是想一想,有必要為了一部分的視圖增加一個(gè)共同的實(shí)現(xiàn)而是全部的UIView的類都具有這個(gè) shake() 方法么围肥,而且可讀性很差剿干,特別是當(dāng)你的UIView的extension中的代碼不停的往下增加變得很冗長(zhǎng)的時(shí)候:
class FoodImageView: UIImageView {
// other customization here
}
class ActionButton: UIButton {
// other customization here
}
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
單獨(dú)看 FoodImageView 類和 ActionButton 類的時(shí)候,你看不出來(lái)它們可以抖動(dòng)穆刻,而且 share() 函數(shù)到處都可以分布置尔。
利用protocol改造
創(chuàng)建 Shakeable 協(xié)議
// Shakeable.swift
import UIKit
protocol Shakeable { }
extension Shakeable where Self: UIView {
func shake() {
// implementation code
}
}
借助protocol extension 我們把 shake() 限定在UIView類中,并且只有遵循 Shakeable 協(xié)議的UIView類才會(huì)擁有這個(gè)函數(shù)的默認(rèn)實(shí)現(xiàn)氢伟。
class FoodImageView: UIImageView, Shakeable {
}
class ActionButton: UIButton, Shakeable {
}
可讀性是不是增強(qiáng)了很多榜轿?通過(guò)這個(gè)類的定義來(lái)知道這個(gè)類的用途這樣的感覺(jué)是不是很棒叶堆?假如產(chǎn)品看到別家的產(chǎn)品輸入密碼錯(cuò)誤之后有個(gè)變暗的動(dòng)畫(huà)画畅,然后讓你加上,這個(gè)時(shí)候你只需要定義另外一個(gè)協(xié)議 比如 Dimmable 協(xié)議:
class FoodImageView: UIImageView, Shakeable, Dimmable {
}
這樣很方便我們重構(gòu)代碼渣锦,怎么說(shuō)呢诚些,當(dāng)這個(gè)視圖不需要抖動(dòng)的時(shí)候设褐,刪掉 shakeable協(xié)議:
class FoodImageView: UIImageView, Dimmable {
}
嘗試從協(xié)議開(kāi)始編程吧!
什么時(shí)候使用class?
- 實(shí)例的拷貝和比較意義不大的情況下
- 實(shí)例的生命周期和外界因素綁定在一起的時(shí)候
- 實(shí)例處于一種外界流式處理狀態(tài)中助析,形象的說(shuō)犀被,實(shí)例像污水一樣處于一個(gè)處理污水管道中。
final class StringRenderer : Renderer {
var result: String
...
}
在Swift中final關(guān)鍵字可以使這個(gè)class拒絕被繼承外冀。
別和框架作對(duì)
- 當(dāng)一個(gè)框架希望你使用子類或者傳遞一個(gè)對(duì)象的時(shí)候寡键,別反抗。
小心細(xì)致一些
- 編程中不應(yīng)該存在越來(lái)越臃腫的模塊雪隧。
- 當(dāng)從class中重構(gòu)某些東西的時(shí)候西轩,考慮非class的處理方式。
總結(jié)
wwdc視頻中明確表示:
Protocols > Superclasses
Protocol extensions = magic (almost)