面向協(xié)議編程

此文為資料匯總文鹊汛,基于自己的理解收集網(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)的一些弊端

姿勢(shì)不錯(cuò)

如圖如何走心动羽。

  • 面對(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)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末脑沿,一起剝皮案震驚了整個(gè)濱河市藕畔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌庄拇,老刑警劉巖注服,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異措近,居然都是意外死亡溶弟,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)瞭郑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辜御,“玉大人,你說(shuō)我怎么就攤上這事屈张∏苋ǎ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵阁谆,是天一觀的道長(zhǎng)菜拓。 經(jīng)常有香客問(wèn)我,道長(zhǎng)笛厦,這世上最難降的妖魔是什么纳鼎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮裳凸,結(jié)果婚禮上贱鄙,老公的妹妹穿的比我還像新娘。我一直安慰自己姨谷,他們只是感情好逗宁,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著梦湘,像睡著了一般瞎颗。 火紅的嫁衣襯著肌膚如雪件甥。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天哼拔,我揣著相機(jī)與錄音引有,去河邊找鬼。 笑死倦逐,一個(gè)胖子當(dāng)著我的面吹牛譬正,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播檬姥,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼曾我,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了健民?” 一聲冷哼從身側(cè)響起抒巢,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎秉犹,沒(méi)想到半個(gè)月后蛉谜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凤优,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年悦陋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜈彼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筑辨。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖幸逆,靈堂內(nèi)的尸體忽然破棺而出棍辕,到底是詐尸還是另有隱情,我是刑警寧澤还绘,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布楚昭,位于F島的核電站,受9級(jí)特大地震影響拍顷,放射性物質(zhì)發(fā)生泄漏抚太。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一昔案、第九天 我趴在偏房一處隱蔽的房頂上張望尿贫。 院中可真熱鬧,春花似錦踏揣、人聲如沸庆亡。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)又谋。三九已至拼缝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彰亥,已是汗流浹背咧七。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留剩愧,地道東北人猪叙。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像仁卷,于是被迫代替她去往敵國(guó)和親穴翩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容