Как правильно разделить ViewModel и ViewController в RAC MVVM

Я только начал обновлять свое приложение ReactiveCocoa, чтобы использовать шаблон MVVM, и у меня есть несколько вопросов, касающихся границы между ViewController и ViewModel и того, насколько глупым должен быть ViewController.

Первая часть приложения, которое я обновляю, - это поток входа в систему, который ведет себя следующим образом.

  • Пользователь вводит адрес электронной почты, пароль и нажимает кнопку входа
  • Успешный ответ содержит один или несколько User модели
  • Эти User модели отображаются вместе с кнопкой выхода
  • User модель должна быть выбрана для сеанса до того, как представление входа в систему будет закрыто, и будет представлен основной вид.

До МВВМ

  • LoginViewController непосредственно обрабатывает LoginButton команда
  • LoginButton Командование говорит непосредственно с SessionManager
  • LoginViewController отображает UIActionSheet для выбора User модель или выход
  • Функции выбора и выхода пользователя из LoginViewController поговорить напрямую с SessionManager

После МВВМ

  • LoginViewModel выставляет команду входа в систему и выбор пользователя и методы выхода
  • LoginViewModel методы выбора пользователя и выхода из системы напрямую связаны с SessionManager
  • LoginViewController реагирует на команду входа в систему LoginViewModel
  • LoginViewController отображает UIActionSheet для выбора User модель или выход
  • Функции выбора и выхода пользователя из LoginViewController поговорить с LoginViewModel

LoginViewModel.h

@interface LoginViewModel : RVMViewModel

@property (strong, nonatomic, readonly) RACCommand *loginCommand;
@property (strong, nonatomic, readonly) RACSignal *checkingSessionSignal;
@property (strong, nonatomic, readonly) NSArray *users;
@property (strong, nonatomic) NSString *email;
@property (strong, nonatomic) NSString *password;

- (void)logout;
- (void)switchToUserAtIndex:(NSUInteger)index;

@end

LoginViewModel.m

@implementation LoginViewModel

- (instancetype)init {
    self = [super init];
    if (self) {
        @weakify(self);

        // Set up the login command
        self.loginCommand = [[RACCommand alloc] initWithEnabled:[self loginEnabled]
                                                    signalBlock:^RACSignal *(id input) {
            @strongify(self);
            [[[SessionManager sharedInstance] loginWithEmail:self.email
                                                    password:self.password]
             subscribeNext:^(NSArray *users) {
                 self.users = users;
             }];

            return [RACSignal empty];
        }];

        // Observe the execution state of the login command
        self.loggingIn = [[self.loginCommand.executing first] boolValue];
    }
    return self;
}

- (void)logout {
    [[SessionManager sharedInstance] logout];
}

- (void)switchToUserAtIndex:(NSUInteger)index {
    if (index < [self.users count]) {
        [[SessionManager sharedInstance] switchToUser:self.users[index]];
    }
}

- (RACSignal *)loginEnabled {
    return [RACSignal
            combineLatest:@[
                RACObserve(self, email),
                RACObserve(self, password),
                RACObserve(self, loggingIn)
            ]
            reduce:^(NSString *email, NSString *password, NSNumber *loggingIn) {
                return @([email length] > 0 &&
                         [password length] > 0 &&
                         ![loggingIn boolValue]);
            }];
}

@end

LoginViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    @weakify(self);

    // Bind to the view model
    RAC(self.controlsContainerView, hidden) = self.viewModel.checkingSessionSignal;
    RAC(self.viewModel, email) = self.emailField.rac_textSignal;
    RAC(self.viewModel, password) = self.passwordField.rac_textSignal;
    self.loginButton.rac_command = self.viewModel.loginCommand;
    self.forgotPasswordButton.rac_command = self.viewModel.forgotPasswordCommand;

    // Respond to the login command execution
    [[RACObserve(self.viewModel, users)
     skip:1]
     subscribeNext:^(NSArray *users) {
         @strongify(self);

         if ([users count] == 0) {
             [Utils presentMessage:@"Sorry, there appears to be a problem with your account."
                         withTitle:@"Login Error"
                             level:MessageLevelError];
         } else if ([users count] == 1) {
             [self.viewModel switchToUserAtIndex:0];
         } else {
             [self showUsersList:users];
         }
     }];

    // Respond to errors from the login command
    [self.viewModel.loginCommand.errors
     subscribeNext:^(id x) {
         [Utils presentMessage:@"Sorry, your login credentials are incorrect."
                     withTitle:@"Login Error"
                         level:MessageLevelError];
     }];
}

- (void)showUsersList:(NSArray *)users {
    CCActionSheet *sheet = [[CCActionSheet alloc] initWithTitle:@"Select Organization"];

    // Add buttons for each of the users
    [users eachWithIndex:^(User *user, NSUInteger index) {
        [sheet addButtonWithTitle:user.organisationName block:^{
            [self.viewModel switchToUserAtIndex:index];
        }];
    }];

    // Add a button for cancelling/logging out
    [sheet addCancelButtonWithTitle:@"Logout" block:^{
        [self.viewModel logout];
    }];

    // Display the action sheet
    [sheet showInView:self.view];
}

@end

Вопросы

  1. Создание дополнительного слоя ViewModel означает, что мне нужно прокси SessionManager звонки. Я думаю, выгода от разделения LoginViewController от SessionManager перевешивает дополнительный код и вызовы функций слоя ViewModel?
  2. LoginViewController имеет знания о User модель для отображения списка пользователей, которые могут быть выбраны. Это нарушает схему MVVM и, конечно, не кажется правильным. Если LoginViewModel извлечь только необходимые свойства User модель требуется LoginViewController и добавить их в словарь, массив которого возвращается в LoginViewController? Или было бы лучше иметь метод на LoginViewModel который возвращает имя пользователя с указанным индексом, позволяя LoginViewController отобразить это имя? Я понимаю, что ViewModel отвечает за преодоление разрыва между моделью и представлением, однако это похоже на двойную обработку. Что касается моей догадки в первом вопросе, я предполагаю, что преимущества разделения этих проблем намного перевешивают то, что кажется немного трудоемким процессом картирования.
  3. Если LoginViewModel вызывает все функциональные возможности, содержащиеся в SessionManager достаточно ли написать тесты против LoginViewModel только или должны тесты также быть написаны специально против SessionManager?

1 ответ

Уже довольно поздно, и я уверен, что вы пошли дальше.

1) перемещение логики программы из вида / элемента управления всегда стоит дополнительных нескольких конусов, которые вам нужно записать в прокси. Смысл MVVM состоит в том, чтобы поощрять разделение интересов и обеспечивать четкий канал данных между View/Controller и Model через ViewModel.

С точки зрения View/Controller, ваши View Models должны выполнять следующую функцию:

Выступать в качестве черного ящика данных, который ваш View/Controller может использовать без выполнения каких-либо бизнес-правил, и всегда предполагать, что данные верны.

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

2) В моих реализациях MVVM я пытаюсь следовать этой парадигме: представление / контроллер, содержащий CollectionView/TableView, является родительским представлением, а ячейки - дочерними представлениями. Таким образом, у вас должен быть родительский ViewModel, который должен инициализировать дочерние ViewModels и управлять ими.

В вашем случае вы не используете представление Коллекция / Таблица, но концепция та же самая. Вам следует попросить у вашего родительского View Model список дочерних ViewModels, которые вы можете передать в другое представление для использования. Следуя пункту ответа № 1, родительская модель представления должна обеспечить правильную инициализацию дочерних моделей представления, чтобы дочернему представлению не приходилось беспокоиться о проверке данных.

3) При тестировании проверки / правил данных вашей модели представления вы можете полностью отключить Session Manager и протестировать только модель представления. Что я делаю, так это создаю утверждения, что заштрихованные / смоделированные функции Session Manager вызываются соответствующим образом в моем модульном тесте.

Другие вопросы по тегам