Рефакторинг в ReactiveCocoa

Итак, я совсем недавно начал работать с ReactiveCocoa и решил, что лучший способ научиться — просто сразу же начать рефакторинг некоторого существующего кода. Я хотел получить некоторую критику и убедиться, что я двигаюсь в правильном направлении.

Итак, в приложении, которое я рефакторинг, у меня есть тонна кода, который выглядит следующим образом:

[self.ff getArrayFromUri:@"/States?sort=name asc" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
    if(!theErr) {
       //do something with theObj
    }
    else {
       //handle the error
    }
}];

В настоящее время я реорганизовал это в ReactiveCocoa следующим образом:

-(void)viewDidLoad {
 //ReactiveCocoa
RACCommand *command = [RACCommand command];
RACSubject *subject = [RACSubject subject];
[[[command addSignalBlock:^RACSignal *(id value) {
    NSError *err;
    NSArray *array = [self.ff getArrayFromUri:@"/States" error:&err];
    err ? [subject sendError:err] : [subject sendNext:array];
    return [RACSignal empty];
}]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]];

[subject subscribeNext:^(NSArray *x) {
    [self performSegueWithIdentifier:kSomeSegue sender:x];
} error:^(NSError *error) {
    NSLog(@"the error = %@", error.localizedDescription);
}];

self.doNotLocation = [UIButton buttonWithType:UIButtonTypeCustom];
[self.doNotLocation setBackgroundImage:[UIImage imageNamed:@"BlackButton_small.png"] forState:UIControlStateNormal];
[[self.doNotLocation rac_signalForControlEvents:UIControlEventTouchUpInside] executeCommand:command];
RAC(self.doNotLocation.enabled) = RACAbleWithStart(command, canExecute);
RAC([UIApplication sharedApplication],networkActivityIndicatorVisible) = RACAbleWithStart(command, executing);    }

Это о том, как я должен это делать, используя RACSubject, или есть лучший способ? Вся эта концепция нова для меня, так как моими единственными языками программирования до сих пор были Java и Objective-C, так что этот функциональный реактивный способ мышления немного сбивает меня с толку.


person terry lewis    schedule 27.05.2013    source источник


Ответы (2)


К сожалению, в приведенном вами примере кода есть пара проблем:

  1. Блок, переданный -addSignalBlock:, возвращает пустой сигнал. Это должно быть предупреждающим флагом, так как почти никогда не возвращаются ненужные значения. В данном случае это означает, что блок выполняет свою работу синхронно. Чтобы избежать блокировки основного потока, вы должны создать сигнал, который работает асинхронно, и вернуть его.
  2. -switchToLatest и -deliverOn: ничего не делают. Большинство операторов сигналов работают только тогда, когда на полученный сигнал подписана подписка. В этом случае он просто исчезает в эфире.

Мы можем решить обе эти проблемы сразу. -addSignalBlock: возвращает сигнал из сигналов, возвращенных в блоке. Если мы вернем что-то значимое, это можно будет обработать вне этого метода.

Прежде всего, это нужно добавить в начало:

@weakify(self);

Когда ниже используется @strongify(self), это предотвращает цикл сохранения. Это необходимо, потому что RACCommand живет столько же, сколько self.

Теперь создание внутренних сигналов:

RACSignal *requestSignals = [command addSignalBlock:^(id value) {
    return [RACSignal start:^(BOOL *success, NSError **err) {
        @strongify(self);

        NSArray *array = [self.ff getArrayFromUri:@"/States" error:err];
        *success = (array != nil);

        return array;
    }];
}];

Внутри блока это просто создает сигнал, который вызовет -getArrayFromUri:error: и вернет свои результаты или ошибку, если она произошла. +start: гарантирует, что работа будет происходить в фоновом режиме.

Из всего этого мы получаем requestSignals, который является сигналом тех созданных сигналов. Это может полностью заменить RACSubject, использовавшийся изначально:

RACSignal *arrays = [[[requestSignals
    map:^(RACSignal *request) {
        return [request catch:^(NSError *error) {
            NSLog(@"the error = %@", error);
            return [RACSignal empty];
        }];
    }]
    flatten]
    deliverOn:RACScheduler.mainThreadScheduler];

Сначала мы преобразуем каждый внутренний сигнал в журнал, а затем игнорируем ошибки. (Это немного сложно, но для этого в будущем может быть добавлен оператор RAC . .)

Затем мы выравниваем сигнал сигналов. В результате получается сигнал arrays, который проходит через значения всех внутренних сигналов. Вот почему мы должны были игнорировать ошибки — если бы какая-либо из них достигла этой точки, мы навсегда перестали бы получать все значения из внутренних сигналов.

Наконец, мы «поднимаем» селектор для вызова:

[self rac_liftSelector:@selector(performSegueWithIdentifier:sender:) withObjects:kSomeSegue, arrays];

Это будет повторно отправлять -performSegueWithIdentifier:sender: всякий раз, когда arrays отправляет новое значение (которое будет NSArray, возвращаемым из сети). Вы можете думать об этом как о вызове метода с течением времени. Это лучше, чем подписка, потому что это упрощает побочные эффекты и управление памятью.

person Justin Spahr-Summers    schedule 28.05.2013
comment
Спасибо за ответ, я ценю это. У меня возникает одна проблема с предоставленным вами кодом во втором и последнем примере. [request catch:] говорит, что ему нужно вернуть RACSignal, а не пустоту. Я просто что-то пропустил там? - person terry lewis; 28.05.2013
comment
А, извините, вы совершенно правы. Вы можете просто вернуть [RACSignal empty] (чтобы указать, что сигнал ошибки должен быть заменен немедленно успешным). Я обновлю свой ответ. - person Justin Spahr-Summers; 28.05.2013
comment
Итак, у меня все обновлено, и это работает, за исключением того, что я получаю значительную задержку (около 3 секунд), а также, похоже, блокирует основной поток (все взаимодействия с пользователем не реагируют) с момента, когда я нажимаю кнопку, чтобы когда prepareForSegue:sender: вызывается. Однако, если я сделаю RACSignal starWithScheduler:block и передам [RACScheduler mainThreadScheduler] для планировщика, он выполнится так, как я и ожидал. Итак, что мне здесь не хватает? Честно говоря, эти вещи заставляют меня чувствовать себя идиотом, может быть, мне следует вернуться в Haskell для Pre-K, прежде чем я займусь этим. - person terry lewis; 29.05.2013
comment
@terrylewis Вы используете последнюю версию приведенного выше кода (с +start: внутри сигнального блока)? Я также добавил -deliverOn: к сглаженному сигналу, чтобы убедиться, что переход выполняется в основном потоке. Извините за всю сложность! Команды, как правило, более сложны в использовании и понимании, чем остальная часть фреймворка. - person Justin Spahr-Summers; 29.05.2013
comment
Да, у меня была последняя версия кода. Вот краткое изложение того, что у меня сейчас работает, ссылка - person terry lewis; 29.05.2013
comment
@terrylewis Что произойдет, если заменить RACScheduler.mainThreadScheduler на [RACScheduler scheduler]? - person Justin Spahr-Summers; 29.05.2013
comment
давайте продолжим это обсуждение в чате - person terry lewis; 29.05.2013
comment
[RACScheduler scheduler] вызывает те же проблемы, что и раньше. +start: — это просто удобный метод для startWithScheduler:block:, который использует [RACScheduler scheduler] для планировщика. - person terry lewis; 29.05.2013
comment
@terrylewis Не могли бы вы отправить проблему? Поведение, с которым вы столкнулись, довольно неожиданно. Запуск в основном потоке должен привести к большей блокировке, чем запуск в фоновом планировщике, поэтому мне не ясно, что может происходить. - person Justin Spahr-Summers; 29.05.2013

По своему опыту работы с фреймворком я обнаружил, что очень мало причин использовать RACSubject напрямую, особенно для таких разовых сигналов, как этот. RACSubjects представляют собой изменяемые сигналы, которые в данном случае вам не нужны, и на самом деле могут увеличить сложность вашего кода. Для вас было бы гораздо лучше вернуть ванильный сигнал (через +[RACSignal createSignal:]) внутри этого командного блока, а затем получить код запроса, составляющий тело сигнала:

[[[command addSignalBlock:^RACSignal *(id value) {
    //
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        //Request code here
            return nil;
    }];
}]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]]; 

Или, что еще лучше, вы можете реорганизовать getArrayFromUri:error:, чтобы вернуть сигнал и избавиться от этого троичного оператора:

 [[[command addSignalBlock:^RACSignal *(id value) {
     return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        //...
        [[self getArrayFromUri:@"/States"]subscribeError:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            [subscriber sendNext:array];
        }];
            return nil;
        }];
  }]switchToLatest]deliverOn:RACScheduler.mainThreadScheduler];

Что касается проблемы подписки на следующей строке, то их можно считать побочными эффектами сигнала, поэтому мы можем сделать их явно таковыми с соответствующими вариантами do:, примененными к сигналу для команды:

    [[[command addSignalBlock:^RACSignal *(id value) {
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            [[[[self getArrayFromUri:@"/States"]doError:^(NSError *error) {
                NSLog(@"the error = %@", error.localizedDescription);
                [subscriber sendError:err];
            }] doNext:^(NSArray *array) {
                [subscriber sendNext:array];
                [self performSegueWithIdentifier:kSomeSegue sender:array];
            }] subscribeCompleted:^{
                [subscriber sendCompleted];
            }];
            return [RACDisposable disposableWithBlock:^{
                 // Cleanup
            }];
        }];
    }]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]]; 

Наконец, поскольку команды работают не так, как сигналы, самые внешние операторы не будут оцениваться (спасибо, @jspahrsummers), поэтому их можно удалить.

[command addSignalBlock:^RACSignal *(id value) {
            return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                [[[[self getArrayFromUri:@"/States"]doError:^(NSError *error) {
                    NSLog(@"the error = %@", error.localizedDescription);
                    [subscriber sendError:err];
                }] doNext:^(NSArray *array) {
                    [subscriber sendNext:array];
                    [self performSegueWithIdentifier:kSomeSegue sender:array];
                }] subscribeCompleted:^{
                    [subscriber sendCompleted];
                }];
                return [RACDisposable disposableWithBlock:^{
                     // Cleanup
                }];
            }];
        }]; 
person CodaFi    schedule 27.05.2013
comment
К сожалению, я не могу провести рефакторинг getArrayFromUri:error, поскольку это сторонний фреймворк, и у меня есть доступ только к заголовкам. Однако я переключил все на то, что у вас есть, и оно работает точно так же. Изучение этого похоже на то, что вы начинаете с самого начала почти заново. - person terry lewis; 27.05.2013
comment
Да, фреймворк, безусловно, имеет болезненно крутую кривую обучения, но как только вы узнаете, как он работает, он становится действительно мощным. - person CodaFi; 27.05.2013
comment
@CodaFi switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]] не работает во всех предоставленных вами примерах, поскольку после этого не происходит подписки. То же самое с вашими -do… методами. - person Justin Spahr-Summers; 28.05.2013
comment
Да, я понимаю (наверное, надо повторить это в теле поста). - person CodaFi; 28.05.2013
comment
В случае do для вашего окончательного примера кода я имею в виду, что сетевой запрос никогда не сработает, потому что там нет подписки. - person Justin Spahr-Summers; 28.05.2013
comment
Да, поэтому я изменю это на subscribeCompleted: и перенесу туда побочные эффекты. - person CodaFi; 28.05.2013