/ ios

Введение в MVVM

Первую работу iOS разработчика я получил в 2011 году. Во время учебы в колледже я уже выполнял несколько iOS заказов, но это была моя первая настоящая работа. Меня наняли как единственного iOS разработчика для создания iPad-приложения с красивым дизайном. Всего за семь недель мы выпустили первую версию и продолжили интеграцию, добавляя больше функций и по существу усложняя код.

Иногда мне казалось, что я не понимал что я делаю. Как хороший программист, я разбирался в паттернах разработки, но я слишком тесно работал с продуктом, чтобы объективно оценивать мои архитектурные решения. Только когда компания решила привлечь еще одного разработчика в команду, мы поняли, что у нас есть проблемы.

Слышали ли вы когда-нибудь о MVC? Некоторые называют его Massive View Controller. Именно так, массивно, это и ощущалось в то время. Не буду углубляться в ненужные детали, достаточно сказать, что если бы мне пришлось делать это снова, то я бы принял другие решения.

Одно из ключевых архитектурных изменений которые я бы мог внести, и внес во все приложения которые я разрабатывал познее, это использование альтернативы Model-View-Controller, называемой Model-View-ViewModel.

Что же такое этот MVVM? Не буду нагружать вас разъяснениями о происхождении названия, давайте просто посмотрим на обычное iOS приложение и получим из него MVVM:

Здесь мы видим типичный MVC. Model предоставляет данные, View предостовляет интерфейс, а View Controller является посредником между ими двумя. Отлично.

Представим на мгновение, что, хотя, View и View Controller технически отличные компоненты, они почти всегда идут рука об руку. Когда последний раз View был совмещен с другим View Controller? Или наоброт? Так почему бы не упростить эту связь?

Это более точно описывает MVC код, который возможно вы уже пишите. Но это не очень помогает в отношении больших View Controller-ов которые, как правило, накапливаются в iOS приложениях. Обычно, MVC приложения имеют много логики внутри View Controller-ов. Да, некоторая логика действительно принадлежит View Controller, но остальная принадлежит типу логики называемой 'Логикой представления' (Presentation logic), в терминологии MVVM - это такие вещи, как преобразование значений из моделий или что показывает View, такие как преобразование NSDate в форматированный NSString.

Кое-что мы упускаем в нашей схеме. Какое-то место, куда мы можем поместить логику представления. Назовем это View Model и поместим его между View/Controller и Model:

Уже лучше! Схема точно описывает, что MVVM - это дополненная версия MVC, где мы соединяем View и Controller и перемещаем логику представления из Controller-а в новый объект - View Model. MVVM звучит сложно, но на деле это "приодетая" версия MVC, архитектурой с которой мы уже знакомы.

Теперь, зная что такое MVVM, ответим на вопрос почему мы должны ее использовать. Для меня основной мотивацией является уменьшение сложности View Controller и создание отдельной логики представления, которую проще тестировать. Давайте посмотрим, как достигнуть этих целей на примерах.

Ниже даны три основных пункта которые, я хочу выделить в этой статье:

  • MVVM совместим с уже существующей архитетурой MVC
  • MVVM делает приложения более тестируемыми
  • MVVM работает лучше с механизмом биндинга

Как мы видели ранее, MVVM, в основном, просто нарядная версия MVC, посмотрим, как его можно включить в обычное приложение с MVC архитекутрой. Возьмем обычную модель Person отправляемую в View Controller:

@interface Person : NSObject

- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;

@property (nonatomic, readonly) NSString *salutation;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSDate *birthdate;

@end

Супер. Теперь давайте представим, что у нас есть PersonViewController, где
viewDidLoad будет заполнять лейблы основываясь на данных модели:

- (void)viewDidLoad {
    [super viewDidLoad];

    if (self.model.salutation.length > 0) {
        self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
    } else {
        self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
    }

    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
    self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
}

Получается довольно няшный MVC. Теперь посмотрим как дополнить это с View Model:

@interface PersonViewModel : NSObject

- (instancetype)initWithPerson:(Person *)person;

@property (nonatomic, readonly) Person *person;

@property (nonatomic, readonly) NSString *nameText;
@property (nonatomic, readonly) NSString *birthdateText;

@end

Наша реализация View Model будет выглядет так:

@implementation PersonViewModel

- (instancetype)initWithPerson:(Person *)person {
    self = [super init];
    if (!self) return nil;

    _person = person;
    if (person.salutation.length > 0) {
        _nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
    } else {
        _nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
    }

    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
    _birthdateText = [dateFormatter stringFromDate:person.birthdate];

    return self;
}

@end

Отлично. Мы переместили логику представления из viewDidLoad в View Model. Наш viewDidLoad метод теперь занимает меньше места:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.nameLabel.text = self.viewModel.nameText;
    self.birthdateLabel.text = self.viewModel.birthdateText;
}

Итак, как вы видите, не так уж много было изменено в архитектуре MVC. Тот же код, просто перемещенный. В сочетании с MVC это ведет к простым View Controller-ам и упрощенной тестирумости.

Тестируемость? Это как? View Controller-ы как правило трудно тестированить, так как они выполняют много задач. В паттерне MVVM мы попробуем переместить много кода в View Model, насколько это возможно. Тестировать View Controller становится намного проще, когда они не так сильно перегружены, а view model очень просто тестированить. Давайте взглянем:

SpecBegin(Person)
    NSString *salutation = @"Dr.";
    NSString *firstName = @"first";
    NSString *lastName = @"last";
    NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0];

    it (@"should use the salutation available. ", ^{
        Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.nameText).to.equal(@"Dr. first last");
    });

    it (@"should not use an unavailable salutation. ", ^{
        Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.nameText).to.equal(@"first last");
    });

    it (@"should use the correct date format. ", ^{
        Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970");
    });
SpecEnd

Если бы мы не переместили эту логику в View Model, нам бы пришлось инициировать весь View Controller со всеми View и сравнивать значения внутри наших лейблов. Это бы не только обощало большое кол-во логики View Controller в одном тесте, но и было бы слишком легко ломаемым. Теперь мы легко можем изменять иерархию View компонентов не боясь сломать тесты ViewModel. Преемущества тестирования View Model-ей очевидны даже для простого примера, и они становятся еще более очевидны с более сложной логикой.

Заметьте что в простом примере модель неизменяема, мы определяем значения View Model в момент инициализации. В изменяемых моделях нам необходимо реализовать какой либо механизм биндингов чтобы View Model могла бы обновлять свои параметры когда произошли изменения. К тому же когда параметры View Model были изменены, необходимо изменить параметры View. Изменения в модели должны каскадно переходить через View Model и потом в View.

На OS X мы можем использовать Cocoa bindings, но у нас нет такой роскоши на iOS. Приходит на ум KVO и он делает отличную работу. Тем не менее это пораждает много шаблонов для простых биндингов, особенно с большим количеством параметров. Вместо этого я предпочитаю использовать ReactiveCocoa, но нет ничего заставляющего использовать в MVVM именно ReactiveCocoa. MVVM это отличный паттерн который становится только лучше с хорошим фреймворком биндингов.

Оригинал

Введение в MVVM
Share this