ReactiveCocoa學習筆記四--設(shè)計指導(譯)

前言

原本這一篇應該在說完RACSignal之后再出的, 但是因為最近事情特別多, RACSignal就一直被耽擱下來了, 而這一篇又是對實際應用RAC框架的一直指導方針, 重要性還是不言而喻的, 因此, 就先留個印象, 看看官方推薦使用RAC框架的正確姿勢.

另外, 想要在團隊中推行RAC框架是很難的一件事情, 為了權(quán)衡, 但是有時候又想去使用一些函數(shù)式的代碼, 因此, 推薦一下RXCollection這個庫, github上這個庫有一個比較復雜的版本, 但是已經(jīng)說不用了, 我個人使用的是精簡版的, 也就是只有map, filter, fold這些高階函數(shù)(概念)的版本, 主要是事先轉(zhuǎn)換一下自己的思維, 把用for循環(huán)寫的代碼, 改用map, fold等等.

正文

本文檔包含了對于想要使用RAC框架的工程的一些設(shè)計指導, 文檔內(nèi)容主要受Rx設(shè)計指導的啟發(fā).

本文檔假定你已基本熟悉RAC的特性. 如果沒有, 那么框架總覽則是一個更好的選擇來快速起步學習RAC的特性.

RACSignal初探

RACSignal是一個推驅(qū)動的流, 通過訂閱來集中處理異步的事件傳遞. 在框架總覽中可以獲取到更多相關(guān)信息.

序列化的信號事件

一個信號可能選擇任意的線程來傳遞事件. 連續(xù)的事件甚至允許抵達不同的線程或者調(diào)度器, 除非顯式地指定傳遞到特定的調(diào)度器.

然而, RAC保證不會有2個信號事件同時到達. 當一個事件正在被處理的時候, 沒有其它的時間會被傳遞. 其余時間的發(fā)送方將會等待, 知道當前的時間已經(jīng)被處理完畢.

這很顯然地意味著, 傳遞給-subscribeNext:error:completed:的block不需要去互相同步, 因為它們絕對不同同步執(zhí)行.

錯誤會立即傳遞

在RAC中, 錯誤事件有異常的語義(譯注: 也就是意味著異常). 當一個錯誤事件被發(fā)送到信號中, 他將會理解被轉(zhuǎn)發(fā)到所有的受依賴信號, 導致整個鏈路終止.

主要目的為改變錯誤事件處理行為的操作符-catch, -catchTo-materialize, 不受此規(guī)則約束.

每次訂閱都發(fā)生副作用

對RACSignal的每一次新的訂閱都會觸發(fā)副作用. 這意味著, 任何副作用都會觸發(fā), 就像有多次訂閱到信號本身一樣. (譯注: 應該是為了說明訂閱幾次就會觸發(fā)幾次副作用 )

考慮下面的例子:

__block int aNumber = 0;
// 含有將`aNumber`自增副作用block的 RACSignal
RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
   aNumber++;
    [subscriber sendNext:@(aNumber)];
    [subscriber sendCompleted];
    return nil;
}];

// 這里將會打印 "subscriber one: 1"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber one: %@", x);
}];

// 這里將會打印 "subscriber two: 2"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber two: %@", x);
}];

副作用會在每次訂閱都重復. 這種行為應用于大多數(shù)操作符:

__block int missilesToLaunch = 0;

// 副作用為每次訂閱都會改變`missToLaunch`的信號
RACSignal *processedSignal = [[RACSignal
    return:@"missiles"]
    map:^(id x) {
        missilesToLaunch++;
        return [NSString stringWithFormat:@"will launch %d %@", missilesToLaunch, x];
    }];

// 這里會打印 "First will launch 1 missiles"
[processedSignal subscribeNext:^(id x) {
    NSLog(@"First %@", x);
}];

// 這里會打印 "Second will launch 2 missiles"
[processedSignal subscribeNext:^(id x) {
    NSLog(@"Second %@", x);
}];

如果要抑制這種行為, 想要信號的副作用只執(zhí)行一次, 發(fā)送時間給一個subject.

副作用可能潛伏很深, 不容易被診斷出來. 為此, 建議盡可能使副作用顯性化

完成或者錯誤事件自動清除訂閱

訂閱者發(fā)送了一個completed或'error'事件, 相關(guān)聯(lián)的訂閱將會被立即清除, 此行為通常是為了杜絕手動清除訂閱的需要.

查看文檔內(nèi)存管理來獲取更多信號聲明周期的信息.

清除會取消處理中的工作并清理資源

當一個訂閱被手動或者自動清除, 任何關(guān)聯(lián)這個訂閱的處理中或者待處理工作都會被盡快優(yōu)雅地取消掉, 任何管理這個訂閱的資源都會被清理掉.

對信號清除訂閱表現(xiàn)在文件上傳上的例子為, 飛行模式下取消網(wǎng)絡(luò)請求, 并從內(nèi)存中釋放文件數(shù)據(jù).

最佳實踐

接下來的建議意圖使基于RAC的代碼更加可預測, 可理解以及高效. 它們只是一些指導意見, 當要決定是否使用這里的建議到某些代碼場景時, 還是要自己做最佳判斷.

為返回信號的方法或者屬性使用描述性聲明

當一個方法或?qū)傩苑祷豏ACSignal時, 很難一眼就理解這個信號的具體語義.

下面有3個關(guān)鍵的問題可以細化聲明:

  1. 是熱信號(在返回給調(diào)用方時就已經(jīng)激活)還是冷信號(訂閱時激活)?
  2. 信號包含0個, 1個還是多個值?
  3. 信號是否有副作用?

無副作用的熱信號應該是一個屬性而不是方法. 屬性的使用暗示了在訂閱信號的事件前是不需要初始化的, 后續(xù)的訂閱者也不會改變這一點. 信號屬性應該被命名在事件之后(例如textChanged)

無副作用的冷信號應該由方法返回, 命名應該是類名詞式的(例如: -currentText). 方法的聲明暗示了信號可能被持有, 工作在訂閱時執(zhí)行. 如果信號發(fā)送多個值, 方法名應該變?yōu)閺蛿?shù)(如: -currentModels).

有副作用的信號應該由動詞式命名的方法返回(例如:-logIn). 動詞暗示著此方法并不是冪等的(譯注: 所謂冪等就是執(zhí)行多次和執(zhí)行一次是一樣的結(jié)果)并且調(diào)用方需要小心調(diào)用, 只有當副作用是期望的才調(diào)用. 如果信號將會發(fā)送一個或多個值, 方法要含有一個名詞(例如: -loadConfiguration, -fetchLatestEvents).

信號操作的不斷縮進

這一段不翻譯了, 主要是因為鏈式調(diào)用的原因, RAC代碼很容易寫的很密集, 因此為了方便理解, 官方給出的一鏈式調(diào)用的縮進例子

RACSignal *result = [[[RACSignal
    zip:@[ firstSignal, secondSignal ]
    reduce:^(NSNumber *first, NSNumber *second) {
        return @(first.integerValue + second.integerValue);
    }]
    filter:^ BOOL (NSNumber *value) {
        return value.integerValue >= 0;
    }]
    map:^(NSNumber *value) {
        return @(value.integerValue + 1);
    }];

以及:

[[signal
    then:^{
        @strongify(self);

        return [[self
            doSomethingElse]
            catch:^(NSError *error) {
                @strongify(self);
                [self presentError:error];

                return [RACSignal empty];
            }];
    }]
    subscribeCompleted:^{
        NSLog(@"All done.");
    }];

同一信號所有值類型統(tǒng)一

RACSignal本身允許信號存在多種類型的對象, 就像Cocoa的集合類一樣. 然而, 在同一個信號中使用多種類型會使操作符的使用變得復雜, 并且增加使用者的負擔, 他們必須要小心地調(diào)用對象方法.

因此, 盡可能保持信號只包含同樣的類型.

只處理所需的信號

無謂地保持RACSignal訂閱只會增加內(nèi)存和CPU的消耗, 不需要的工作就不應該執(zhí)行.

如果只有特性的值需要通過信號傳遞, -take:操作符可以用來取回這些值, 并在之后立即自動清除訂閱.

類似-take:的操作符和-takeUntil:自動清理棧. 如果除了剩下的值并不需要其余的東西, 任何依賴也都將被終止, 潛移默化地節(jié)省了一大堆工作.

傳遞信號事件到已知調(diào)度器

當信號被方法返回, 或者與另一個信號結(jié)合, 會難以得知信號從哪個線程傳遞過來. 盡管保證事件是序列執(zhí)行的, 有時候也會需要有更強力的保證, 比如要執(zhí)行更新UI代碼的時候(必須在主線程執(zhí)行).

一旦這個保證很重要的時候, -deliverOn:操作符可以用來強制使信號的事件抵達到指定的調(diào)度器

盡量少切換調(diào)度器

盡管上面說了使事件在指定調(diào)度器傳遞是非常必要的, 但是切換調(diào)度器也會引入不必要的延遲以及導致CPU負載升高.

一般情況下, -deliverOn應該限制出現(xiàn)在信號鏈最后面, 理想位置應該在訂閱前, 或者在值綁定到一個屬性之前.

使信號的副作用顯性化

如果可以的話, 盡量不要讓RACSignal有副作用, 因為訂閱者可能會發(fā)現(xiàn)副作用的行為并不是想要的.

然而, 因為Cocoa主要是命令式編程, 有時候在信號事件發(fā)生時需要做點什么. 雖然大多數(shù)操作符接受任意block(這里可能包含副作用), 使用-doNext:, -doError:-doCompleted:將使副作用更加顯性化和自文檔化:

NSMutableArray *nexts = [NSMutableArray array];
__block NSError *receivedError = nil;
__block BOOL success = NO;

RACSignal *bookkeepingSignal = [[[valueSignal
    doNext:^(id x) {
        [nexts addObject:x];
    }]
    doError:^(NSError *error) {
        receivedError = error;
    }]
    doCompleted:^{
        success = YES;
    }];

RAC(self, value) = bookkeepingSignal;

用主題(Subject)共享信號的副作用

默認每次訂閱都會發(fā)生副作用, 但是某些情況下, 副作用只需要發(fā)生一次即可, 例如一次典型的網(wǎng)絡(luò)請求不應該在新訂閱者加進來后再重復一次.

與其訂閱一個信號多次, 不如轉(zhuǎn)發(fā)信號的事件到RACSubject, 這一可以想訂閱幾次就訂閱幾次:

// 這個信號在每次訂閱都會開啟一個新請求
RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    AFHTTPRequestOperation *operation = [client
        HTTPRequestOperationWithRequest:request
        success:^(AFHTTPRequestOperation *operation, id response) {
            [subscriber sendNext:response];
            [subscriber sendCompleted];
        }
        failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            [subscriber sendError:error];
        }];

    [client enqueueHTTPRequestOperation:operation];
    return [RACDisposable disposableWithBlock:^{
        [operation cancel];
    }];
}];

// 這個主題會分發(fā)`networkRequest`信號的事件
RACSubject *results = [RACSubject subject];

// 設(shè)置一堆訂閱者給主題
[results subscribeNext:^(id response) {
    NSLog(@"subscriber one: %@", response);
}];

[results subscribeNext:^(id response) {
    NSLog(@"subscriber two: %@", response);
}];

// 然后, 真正地開始請求(通過訂閱一次請求的信號), 并且2個訂閱者都能接受到一樣的事件
[networkRequest subscribe:results];

給定信號名字來調(diào)試

每個信號都一個name屬性來輔助調(diào)試. 信號的-description包含了它的名字, 并且所有的RAC操作符都自動會添加名字. 一般情況下, 默認的名字就基本可以鑒別信號了.

例如:

RACSignal *signal = [[[RACObserve(self, username) 
    distinctUntilChanged] 
    take:3] 
    filter:^(NSString *newUsername) {
        return [newUsername isEqualToString:@"joshaber"];
    }];

NSLog(@"%@", signal);

將會打印出[[[RACObserve(self, username)] -distinctUntilChanged] -take: 3] -filter:.
名字也可以用-setNameWithFormat:來手動設(shè)定.

RACSignal也提供了-logNext, -logError, -logCompleted-logAll這些方法, 可以在事件發(fā)生時自動打印信號日志. 這可以很方便地實時審查信號.

避免顯式地訂閱和解除

雖然-subscribeNext:error:completed:與其變形方法是最基本的處理信號的方式, 但是直接使用會使得代碼變得復雜, 因為缺少陳述聲明, 副作用的濫用, 以及潛在的內(nèi)置功能.

同樣地, 顯式使用RACDisposables類可能是迅速導致資源管理和清理的代碼的源頭.(譯注: rat's nest翻譯為了源頭)

這里有一些更加高階的模式來替代手動訂閱和解除:

  • RAC()RACChannelTo宏可以用來綁定信號到屬性上, 而不是在改變發(fā)生時手動地更新.

  • -rac_liftSelector:withSignals:方法可以用來自動執(zhí)行某個方法當一個或者多個信號觸發(fā)后.

  • 類似-takeUntil:可以來用在事件發(fā)生時自動解除訂閱(例如"取消"按鈕被按下了)

通常, 相比于在訂閱的回調(diào)中復制同樣的行為, 使用內(nèi)置的操作符可以使代碼更加簡潔和更加健壯.

避免直接操作主題

主題是橋接指令式代碼到信號的世界中和共享副作用的強大工具, 但是, 作為RAC中的"可變量", 主題很容易因為濫用而導致復雜度提升.

因為主題可以在任何地方任何時候操作, 經(jīng)常會打破線性流處理和使得邏輯很難追蹤. 同時主題還不支持有意義的解除, 這樣會導致不必要的工作.

在下列情況下, 主題可以被替換成其它的RAC模式:

然而, 主題在共享信號的副作用上還是很必要的. 在這種情況下, 使用-Subscribe:并且避免調(diào)用-sendNext:, -sendError-sendCompleted來直接操作主題.

實現(xiàn)新的操作符

RAC提供了一大串的內(nèi)置操作符來覆蓋RACSignal的大多數(shù)應用場景, 然而, RAC并不是封閉的系統(tǒng). 針對特定使用場景實現(xiàn)額外的操作符來是完全有效的, 甚至可以針對RAC本身來實現(xiàn).

實現(xiàn)新的操作符需要小心細節(jié), 并且專注于簡潔, 避免引入bug到調(diào)用代碼中.

下面的指導覆蓋了一些常見的陷阱, 且有助于保護期望的API的規(guī)約.
(譯注: 大多數(shù)情況下我們不會去自定義新的操作符, 所以這節(jié)就簡單翻譯要點, 如果有需要查看完整內(nèi)容的, 還是查看源文檔更好)

  • 盡量組合現(xiàn)有的操作符: 這些操作符已經(jīng)經(jīng)過了嚴格的測試和實際工程檢驗了.
  • 避免引入并發(fā): 并發(fā)是很常見的bug之源, 代碼的并發(fā)應該交由調(diào)用方來決定.
  • 在解除訂閱后取消工作和清理資源
  • 不要在操作符中阻塞: 信號操作符應該理解返回一個新的信號, 操作符需要做的任何工作都應該成為訂閱一個新信號的一部分, 而不是本身的.
// WRONG!
- (RACSignal *)map:(id (^)(id))block {
    RACSignal *result = [RACSignal empty];
    for (id obj in self) {
        id mappedObj = block(obj);
        result = [result concat:[RACSignal return:mappedObj]];
    }

    return result;
}

// Right!
- (RACSignal *)map:(id (^)(id))block {
    return [self flattenMap:^(id obj) {
        id mappedObj = block(obj);
        return [RACSignal return:mappedObj];
    }];
}
  • 避免深度遞歸造成棧溢出: 可能出現(xiàn)無限遞歸的地方都應該使用RACScheduler的-scheduleRecursiveBlock:方法.
    錯誤姿勢:
- (RACSignal *)repeat {
    return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

        __block void (^resubscribe)(void) = ^{
            RACDisposable *disposable = [self subscribeNext:^(id x) {
                [subscriber sendNext:x];
            } error:^(NSError *error) {
                [subscriber sendError:error];
            } completed:^{
                resubscribe();
            }];

            [compoundDisposable addDisposable:disposable];
        };

        return compoundDisposable;
    }];
}

正確姿勢:

  -(RACSignal *)repeat {
    return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

        RACScheduler *scheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler];
        RACDisposable *disposable = [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) {
            RACDisposable *disposable = [self subscribeNext:^(id x) {
                [subscriber sendNext:x];
            } error:^(NSError *error) {
                [subscriber sendError:error];
            } completed:^{
                reschedule();
            }];

            [compoundDisposable addDisposable:disposable];
        }];

        [compoundDisposable addDisposable:disposable];
        return compoundDisposable;
      }];
      }

后記

限于水平, 文中難免出現(xiàn)錯漏, 如果你發(fā)現(xiàn)了, 麻煩評論指出.

同時, 通篇看完后, 還是要啰嗦一句, 如果真打算在實際工程中使用RAC, 這篇是必不可少的, 剩下的一篇比較關(guān)鍵的內(nèi)存管理其實結(jié)論還是比較簡單的, 找機會直接在一篇文章中插入即可.

最后, 希望對函數(shù)響應式編程感興趣的同學一起進步.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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