面向協(xié)議編程

此文為資料匯總文,基于自己的理解收集網(wǎng)絡(luò)上簡明易懂的文章及例子,通篇瀏覽之后會(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)楹啎腗arkdown 不支持 [toc]生成目錄,另外一種方式生成目錄較為繁瑣,所以貼個(gè)圖片版的目錄,另外希望簡書早點(diǎn)支持[toc]:

目錄

什么叫做面向協(xié)議編程

我自己感覺比較明確的定義就是Apple wwdc15/408視頻中的那句話:

Don't start with a class.
Start with a protocol.

從協(xié)議的角度開始編程??

談?wù)劽鎸?duì)對(duì)象編程(OOP)的一些弊端

姿勢不錯(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í)的世界來考慮這個(gè)問題,我們對(duì)客觀存在的主體看法是會(huì)隨著時(shí)間的改變而改變的,真實(shí)世界中甚至不存在固定形式化的抽象,而代碼是為了具體問題而出現(xiàn)的,所以不存在通用抽象,也就不存在可以無限重用的定義和邏輯。所以對(duì)象也就是用于計(jì)算的模型而已,技術(shù)手段是正確的(給數(shù)據(jù)綁定操作) 但是對(duì)于目標(biāo)(大規(guī)模代碼重用)相去甚遠(yuǎn),能重用的應(yīng)該只有為了解決問題的方法,而不是只有模型。另外的難點(diǎn),不同人為了解決相似問題,開發(fā)出來的模型可以十萬八千里,為了重用模型,修改之后又能適應(yīng)新問題,于是這叫泛化,它估計(jì)你去制造全能模型,但是不僅難,還沒法向后兼容,有時(shí)候就硬是要把飛機(jī)做成魚……這就是面向?qū)ο笏季S的硬傷,創(chuàng)造出一個(gè)大家都懂,大家都認(rèn)為對(duì),大家都能拿去用的模型太難!(摘自知乎精選)

我自己的感覺,類的繼承讓代碼的可讀性大大降低,比如我想知道這個(gè)類用途還要去看這個(gè)類的父類能干嘛假如它還有個(gè)祖父類呢?而且想想看假如一個(gè)項(xiàng)目由一個(gè)基類開始,并伴生了很多子類,解決需求發(fā)現(xiàn)需要更改基類的時(shí)候,不敢動(dòng)手是多么恐怖的一件事情。

Java程序員對(duì)單個(gè)方法的實(shí)現(xiàn)超過10行感到非常不安,這代表自己的代碼可重用性很差。于是他把一個(gè)3個(gè)參數(shù)的長方法拆成了4個(gè)子過程,每個(gè)子過程有10個(gè)以上的參數(shù)。后來他覺得這樣很不OOP,于是他又創(chuàng)建了4個(gè)interface和4個(gè)class。

由一個(gè)簡單的例子開始

讓我們由這個(gè)例子開始面向“協(xié)議”編程

例子采用Rust語言,編輯器推薦使用CodeRunner

先用面對(duì)對(duì)象的視角,書可以燃燒,于是書有個(gè)方法 burn()。
書并不是唯一會(huì)燃燒的東西,木頭也可以燃燒,它也有一個(gè)方法叫做 burn()??纯床皇敲嫦颉皡f(xié)議”下是如何燃燒:

struct Book {
    title: @str,
    author: @str,
}

struct Log {
    wood_type: @str,
}

這兩個(gè)結(jié)構(gòu)體分別表示書(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)在書與木頭都有了 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ù),是否我們可以把書也傳進(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è)問題,再寫一個(gè)函數(shù)接受Book結(jié)構(gòu)體?這樣只會(huì)得到兩個(gè)幾乎一樣的函數(shù)。

解決這個(gè)問題

加一個(gè)協(xié)議接口,協(xié)議接口在Rust語言中叫做 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));
    }
}

接下來實(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é)議接口的類型就沒有問題。主函數(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é)議)。

目前大部分開發(fā)以面向?qū)ο缶幊虨橹?,比如使?ASIHttpRequest 來執(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)在來看看 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)心它有哪些屬性,除非接口無法滿足需求需要去了解相關(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

通過接口的定義,調(diào)用者可以不再關(guān)心ApiService對(duì)象,也無需了解其有哪些屬性。即使需要重構(gòu)替換新的對(duì)象,調(diào)用邏輯也不受任何影響。調(diào)用接口往往比訪問對(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可以定義方法,可是屬性的問題怎么解決?此時(shí),裝飾器模式也許正好可以解決該問題,讓我們?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)過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)過多的if-else代碼,也保證了代碼的整潔性。

依賴注入

GetApiService依然是以對(duì)象依賴的形式存在,如何解決這個(gè)問題?沒錯(cuò),依賴注入!依賴注入會(huì)讓測試變得可行。

關(guān)于依賴注入可以看這篇文章

借助 objection 開源庫改造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)的接口,各司其職,互不干涉。

我自己的感覺,利用OC來進(jìn)行面向協(xié)議編程還是繞不過這個(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ì)于這種情況,蘋果的工程師表示這是一種 Lost Type Relationships,我對(duì)這個(gè)的理解 是 失去對(duì)類型的控制。也就是這個(gè)Number類的函數(shù)往里邊傳非Number類型的參數(shù)會(huì)出問題??赡苣銈冇X得這個(gè)問題還好,只要注意下Number下函數(shù)的函數(shù)實(shí)現(xiàn)就好了,但是在大型項(xiàng)目中,你使用一個(gè)類因?yàn)閾?dān)心類型問題而需要去看類的實(shí)現(xiàn),這樣的編碼是不是很讓人煩躁?

利用Protocol來重寫

直接上代碼吧:

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)體)來取代class
protocol 的Self表示任何遵循了這個(gè)協(xié)議的類型?,F(xiàn)在就不用擔(dān)心類型的問題了。

struct與class的區(qū)別

struct是值拷貝類型,而class是引用類型。這也是apple的工程師推薦使用struct代替class的原因。

struct無法繼承,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)樨悹柫?

再插入一幅圖來理解引用類型吧:

簡單的運(yùn)用下我們定義的這個(gè)協(xié)議吧

以下是一個(gè)簡單的二分查找算法函數(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é)議的類型,這里就和開頭使用Rust語言實(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)在我們回過頭來想想,使用OC編程中,系統(tǒng)固有的協(xié)議不借助黑魔法我們是否可以對(duì)已有的協(xié)議進(jìn)行擴(kuò)展?不能?。P(guān)于在OC中如何擴(kuò)展協(xié)議自行搜索,此處不展開了)。

一個(gè)簡單的例子運(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)在來定義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
}

在將來,你需要改動(dòng)代碼,比如 Parrot 老了,沒辦法飛了,就將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è)簡單的實(shí)戰(zhàn)

這樣一個(gè)簡單的需求,一個(gè)登陸頁面,用戶輸入的密碼錯(cuò)誤之后,密碼框會(huì)有一個(gè)抖動(dòng),實(shí)現(xià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)畫時(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")
    }
}

這樣看起來似乎已經(jīng)很棒了,因?yàn)槲覀児?jié)約了代碼,但是想一想,有必要為了一部分的視圖增加一個(gè)共同的實(shí)現(xiàn)而是全部的UIView的類都具有這個(gè) shake() 方法么,而且可讀性很差,特別是當(dāng)你的UIView的extension中的代碼不停的往下增加變得很冗長的時(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í)候,你看不出來它們可以抖動(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)了很多?通過這個(gè)類的定義來知道這個(gè)類的用途這樣的感覺是不是很棒?假如產(chǎn)品看到別家的產(chǎn)品輸入密碼錯(cuò)誤之后有個(gè)變暗的動(dòng)畫,然后讓你加上,這個(gè)時(shí)候你只需要定義另外一個(gè)協(xié)議 比如 Dimmable 協(xié)議:

class FoodImageView: UIImageView, Shakeable, Dimmable {

}

這樣很方便我們重構(gòu)代碼,怎么說呢,當(dāng)這個(gè)視圖不需要抖動(dòng)的時(shí)候,刪掉 shakeable協(xié)議:

class FoodImageView: UIImageView, Dimmable {

}

嘗試從協(xié)議開始編程吧!

什么時(shí)候使用class?

  • 實(shí)例的拷貝和比較意義不大的情況下
  • 實(shí)例的生命周期和外界因素綁定在一起的時(shí)候
  • 實(shí)例處于一種外界流式處理狀態(tài)中,形象的說,實(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)該存在越來越臃腫的模塊。
  • 當(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容