Давайте рассмотрим преимущества Cycle.js и Model-View-Intent.

В нынешней экосистеме разработки программного обеспечения неудивительно, что архитектура модель-представление-контроллер (MVC) не пользуется большой репутацией. Распространенные альтернативы набирают популярность, такие как Model-View-Presenter (MVP) и Model-View-ViewModel (MVVM).

Как мобильный разработчик я пробовал архитектуру MVP. Фактически, у меня был лучший опыт из-за разделения задач и улучшенной тестируемости, обеспечиваемой этой архитектурой. Но он не предлагает паттерна для потока данных (например, Flux или Redux), и я был этим как-то недоволен. Мне было интересно, есть ли способ свести к минимуму ошибки и улучшить взаимодействие с разработчиками.

Модель-представление-намерение (MVI)

Первой концепцией, которая привлекла мое внимание, была реализация Model-View-Intent (MVI) на Android, предложенная библиотекой Mosby. Я решил прочитать его код и попытаться понять его принципы.

Мосби выглядел как отличная библиотека, особенно потому, что ее создатель тщательно задокументировал ее мотивацию и опубликовал примеры в своем блоге. Но, к сожалению, Мосби казался слишком сложным. У него была крутая кривая обучения, и это было не совсем то, что я пытался найти, и представляло собой лишь небольшое постепенное улучшение по сравнению с MVP.

Концепция MVI была впервые введена не Мосби, а скорее веб-фреймворком под названием Cycle.js. Поэтому я решил изучить основы. К моему удивлению, Cycle.js мне понравилась идея MVI, и я хочу попробовать. В основном потому, что фреймворк очень маленький и простой.

Вот основные принципы MVI и почему они имеют большое значение:

  • Чисто реактивный: это значительно упрощает координацию асинхронных задач и дает все преимущества декларативного программирования. В случае Cycle.js он делает ваше представление тестируемым. Как мы увидим ниже, представление становится обычным наблюдаемым .
  • Однонаправленный поток данных: в MVI данные следуют по прямому пути: намерение, модель и представление. Я подробно расскажу об этом в следующем разделе. Но пока это означает, что вы, как разработчик, должны научиться организовывать свой код для использования этого шаблона. Как только вы преодолеете кривую обучения, ваше приложение станет проще для понимания. Каждая функция в вашем приложении следует одному и тому же рецепту.
  • Слой представления представлен одним объектом, моделью: все состояние представления представлено уникальным источником истины, включая загрузка и ошибка. Это означает, что вы должны смотреть и манипулировать одним местом для правильного отображения представления .

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

MVI в реальном приложении

Получив краткое представление о MVI, я решил создать приложение с использованием Cycle.js, чтобы проверить его преимущества на практике. Созданное мной приложение предоставляет начальный список символов, а затем выполняет поисковые запросы по Star Wars API, когда вы вводите что-то во входном тексте. Вы можете увидеть код в этом репозитории.

Основная структура приложения Cycle.js - это абстракция концепции взаимодействия человека с компьютером. Это представлено единственной функцией, в которой любое внешнее взаимодействие передается в качестве параметра функции (обычно называемого источниками), а человеческий вывод - это объект, возвращаемый функцией (обычно называемый приемниками).

В нашем приложении это представлено методом App в файле app.js. Код, помещенный между входом и выходом, преобразует "источники" в наблюдаемое намерение ,, которое преобразуется в модель observable. Затем последний преобразуется в view observable, который возвращается внутри объекта "стоки".

export function App (sources) {
  // ...
  return sinks;
}

Мы будем строить каждый слой постепенно в том же порядке, в котором должны передаваться данные.

Намерение

Объект намерения содержит наблюдаемые, сгенерированные из объекта "источники". Он представляет намерение пользователя при взаимодействии с приложением. В нашем приложении пользователь может делать две вещи:

  • Введите поисковый запрос, введя введенный текст
  • Получать данные персонажей из API
const intents = {
  receiveCharacterList: sources.HTTP.select(‘api’).flatten(),
  changeSearchTerm: sources.DOM.select(‘#search.form-control’)
    .events(“input”)
    .map(ev => ev.target.value)
    .startWith(‘’)
}

Не беспокойтесь, если вы не понимаете свойство receiveCharacterList объекта intents. На данный момент, чтобы понять концепцию MVI, вам просто нужно понять следующее: changeSearchTerm получает новый наблюдаемый всякий раз, когда пользователь вводит что-то во входных данных с идентификатором ' search.form-control. ' По умолчанию он начинается с пустой строки.

Модель

модель, как я упоминал выше, представляет собой представление текущего состояния представления. Это зависит только от объекта намерений.

const model = Observable.combineLatest(
  intents.receiveCharacterList, 
  intents.changeSearchTerm)
  .map((combined) => {
    const [response, searchTerm] = combined
    return {
      characters: response.body.results,
      searchTerm: searchTerm
    };
 })
 .startWith({
   characters: [{name: ‘Loading…’}],
   searchTerm: ‘’
 });

Здесь мы объединяем наблюдаемый , содержащий ответ API, с наблюдаемым , содержащим строку , типизированную. Результатом является новая наблюдаемая, содержащая список символов и поисковый запрос.

Вид

Представление в Cycle.js не представлено HTML или уровнем контроллера , как мы обычно видим в мобильных приложениях. Конфигурация Cycle.js по умолчанию использует библиотеку под названием Cycle DOM, которая может генерировать наблюдаемое из абстракции виртуальной DOM.

const view = model.map((state) => {
  const list = state.characters.map( character => {
    return tr(td(character.name));
  });
  return div(“.card”, [
    div(‘.card-header’, [
      h4(‘.title’, ‘Star Wars Character Search’),
      input(‘#search.form-control’, {props: {type: “text”, placeholder: “Type to search”, value: state.searchTerm}})
    ]),
    div(‘.card-content .table-responsive’,[
      table(‘.table’, [
        thead(tr(th(h5(‘Name’)))),
        tbody(list)
      ])
    ])
  ]);
});

Как я упоминал выше, представление зависит только от модели . Оно генерирует HTML-таблицу для перечисления символов и заполняет input с введенной строкой.

В конце нашей функции «App» представление является частью возвращаемого объекта «стоков». «Приемники» также должны содержать конфигурацию HTTP-запроса к API:

return {
  DOM: view,
  HTTP: intents.changeSearchTerm.map( searchTerm => {
    return {
      url: ‘https://swapi.co/api/people/?search=' + searchTerm,
      category: ‘api’,
    }
  })
};

Модульное тестирование представления

Учитывая, что представление представления - это просто функция модели , мы можем легко написать для нее модульные тесты. Сначала я извлек создание представления в метод и переместил его в отдельный файл. Это позволило мне использовать его в приложении и в тестах. Затем я использовал пакет chai-virtual-dom для сравнения двух представлений .

Реализованные мной тесты имеют следующую базовую структуру:

  1. Создайте фиктивную модель состояния, которое мы хотим протестировать.
  2. Используйте функцию view , передающую созданный макет , чтобы сгенерировать его представление.
  3. Убедитесь, что созданное представление равно ожидаемому.

В этом приложении я создал два простых тестовых примера:

  • Когда приложение загружает данные API, в представлении должно отображаться состояние загрузка :
const model = Observable.of({
 characters: [{name: ‘Loading…’}],
 searchTerm: ‘’
});
const view = view(model);
const expected = div(".card", [
  div('.card-header', [
    h4('.title', 'Star Wars Character Search'),
    input('#search.form-control', {props: {type: "text", placeholder: "Type to search"}})
  ]),
  div('.card-content .table-responsive',[
    table('.table', [
      thead(tr(th(h5('Name')))),
        tbody([
          tr(td('Darth Vader')),
          tr(td('Darth Maul')),
        ])
      ])
    ])
  ]);
expect(view).to.look.exactly.like(expected);
  • Когда приложение получило данные символов от API, представление должно отобразить их:
const model = Observable.of({
  characters: [{name: 'Darth Vader'}, {name: 'Darth Maul'}],
  searchTerm: 'darth'
});
const view = view(model);
const expected$ = div(".card", [
  div('.card-header', [
    h4('.title', 'Star Wars Character Search'),
    input('#search.form-control', {props: {type: "text", placeholder: "Type to search"}})
  ]),
  div('.card-content .table-responsive',[
    table('.table', [
      thead(tr(th(h5('Name')))),
        tbody([
          tr(td('Darth Vader')),
          tr(td('Darth Maul')),
        ])
      ])
    ])
  ]);
expect(view).to.look.exactly.like(expected);

Заключение

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

В конце концов, MVI не требует больших усилий для изучения и кажется лучшим выбором по сравнению с MVP.

А как насчет Cycle.js? Я еще не уверен на 100%, что смогу начать создавать производственное приложение с помощью Cycle.js. Я думаю, что мне нужно глубже изучить фреймворк, чтобы оценить его реальные возможности, такие как создание маршрутов или система аутентификации.

Вам понравилась эта статья? Если да, пожалуйста, хлопните меня в ладоши, чтобы это увидело больше людей. Спасибо!