Статьи

Шаблон проектирования MVC в ванильном JavaScript

Разработчик с ноутбуком перед голливудским знаком - шаблон проектирования MVC

Шаблоны проектирования часто включаются в популярные фреймворки. Например, шаблон проектирования Model-View-Controller (MVC) является повсеместным. В JavaScript трудно отделить структуру от шаблона проектирования. Часто конкретная структура будет сопровождаться собственной интерпретацией этого шаблона проектирования. Рамки приходят с мнениями, и каждый заставляет вас думать определенным образом.

Современные фреймворки определяют, как выглядит конкретная реализация шаблона MVC. Это сбивает с толку, когда все интерпретации разные, что добавляет шум и хаос. Когда любая кодовая база принимает больше чем одну структуру, это создает расстраивающий беспорядок. Вопрос в моей голове, есть ли лучший способ?

Читать современный JavaScript
Будьте в курсе развивающегося мира JavaScript

Шаблон MVC хорош для клиентских фреймворков, но современные фреймворки меняются. То, что сегодня современно, подвержено временам и отмирает. В этом примере я хотел бы изучить альтернативы и посмотреть, куда нас ведет небольшая дисциплина.

Сам по себе паттерн MVC насчитывает более нескольких десятилетий. Это делает его хорошим шаблоном проектирования, в который можно вложить свои навыки программирования. Шаблон MVC — это шаблон проектирования, который может быть сам по себе. Вопрос в том, как далеко это может нас завести?

Подождите, это еще одна структура?

Во-первых, я хотел бы развеять этот распространенный миф: шаблон дизайна не является основой. Шаблон проектирования — это дисциплинированный подход к решению проблемы кода. Необходим необходимый уровень квалификации, который возлагает ответственность на программиста. Шаблон дизайна разделяет проблемы и продвигает чистый код.

Фреймворк отличается тем, что не должен придерживаться какого-либо шаблона проектирования. Чтобы отличить структуру от шаблона, нужно искать принцип Голливуда. Голливудский принцип гласит: «Не звоните нам, мы вам позвоним». Каждый раз, когда есть зависимость, которая диктует, когда вы ее используете, это фреймворк. Фреймворк очень похож на Голливуд в том, что вы не можете сказать, что делать или как это сделать. Фактически, разработчики похожи на актеров, потому что они следуют сценарию, когда его просят действовать.

Есть веские причины избегать клиентских платформ:

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

Шаблон MVC

Шаблон проектирования MVC возник из исследовательского проекта Xerox Smalltalk в 1970-х и в 80-х годах. Это образец, который выдержал испытание временем для графических пользовательских интерфейсов. Этот паттерн исходил от настольных приложений, но доказал свою эффективность и для веб-приложений.

Суть модели проектирования MVC заключается в четком разделении проблем. Идея состоит в том, чтобы сделать решение понятным и привлекательным. Любой соратник, желающий внести конкретные изменения, может легко найти нужное место.

Демо Пингвинов

Пингвины! Милые и приятные, некоторые из самых ярых существ на планете. Так мило, на самом деле, есть 17 различных видов пингвинов, которые не все живут в условиях Антарктики.

Время для демонстрации пингвинов! Я покажу колоду с несколькими видами на одной странице. Для этого я хотел бы использовать шаблон проектирования MVC и небольшую дисциплину. Я буду использовать методологию экстремального программирования, чтобы решить проблему с помощью модульных тестов, а не чепухи. В конце вы сможете пролистать несколько пингвинов, каждый со своими данными и профилем.

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

Я остановлюсь на ES5 для этой демонстрации по причинам кросс-браузерной совместимости. Имеет смысл использовать проверенные языковые возможности с этим постоянным шаблоном дизайна.

Вы готовы? Давай выясним.

Скелет

Демо будет состоять из трех основных частей: контроллер, вид и модель. У каждого свои заботы и проблемы, которые нужно решить.

Визуальное представление о том, как это выглядит ниже:

Демо Пингвинов Визуал

PenguinController Он определяет, что происходит, когда пользователь выполняет действие (например, нажатие кнопки или нажатие клавиши). Специфичная логика на стороне клиента может идти в контроллере. В большей системе, где много всего происходит, вы можете разбить его на модули. Контроллер является точкой входа для событий и единственным посредником между представлением и данными.

PenguinView DOM — это API-интерфейс браузера, который вы используете для HTML-манипуляций. В MVC никакая другая часть не заботится об изменении DOM, кроме представления. Представление может присоединять пользовательские события, но оставляет проблемы обработки событий для контроллера. Основная директива представления заключается в изменении состояния того, что пользователь видит на экране. Для этой демонстрации представление выполнит манипуляции с DOM в простом JavaScript.

Модель PenguinModel В клиентском JavaScript это означает Ajax. Одним из преимуществ шаблона MVC является то, что теперь у вас есть единственное место для вызовов Ajax на стороне сервера. Это делает его привлекательным для коллег-программистов, которые не знакомы с решением. Модель в этом шаблоне проектирования заботится только о JSON или объектах, которые поступают с сервера.

Одним из анти-паттернов является нарушение этого внутреннего разделения интересов. Модель, например, не должна заботиться о HTML. Вид не должен заботиться об Ajax. Контроллер должен служить посредником, не беспокоясь о деталях реализации.

С этим шаблоном я нахожу, что разработчики начинают с благих намерений, но с проблемами утечки Соблазнительно превратить все в веб-компонент и в конечном итоге кашлять. Акцент делается на функции и проблемы, с которыми сталкиваются пользователи. Но функциональные проблемы не совпадают с функциональными проблемами.

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

Это не было бы большой демонстрацией без реального живого примера, который вы можете увидеть и потрогать. Итак, без дальнейших церемоний, ниже CodePen демонстрирует демо пингвинов:

Хватит разговоров, время для кода.

Контроллер

Представление и модель являются двумя компонентами, используемыми контроллером. Контроллер имеет в своем конструкторе все компоненты, необходимые для работы:

 var PenguinController = function PenguinController(penguinView, penguinModel) {
  this.penguinView = penguinView;
  this.penguinModel = penguinModel;
};

Конструктор использует инверсию управления и таким образом вводит модули. Этот шаблон позволяет внедрить любой компонент, который соответствует высокоуровневому контракту. Думайте об этом как о хорошем способе абстрагировать код от деталей реализации. Этот шаблон позволяет вам писать чистый код на простом JavaScript.

Затем пользовательские события подключаются и обрабатываются следующим образом:

 PenguinController.prototype.initialize = function initialize() {
  this.penguinView.onClickGetPenguin = this.onClickGetPenguin.bind(this);
};

PenguinController.prototype.onClickGetPenguin = function onClickGetPenguin(e) {
  var target = e.currentTarget;
  var index = parseInt(target.dataset.penguinIndex, 10);

  this.penguinModel.getPenguin(index, this.showPenguin.bind(this));
};

Обратите внимание, что это событие использует текущую цель, чтобы получить состояние, сохраненное в DOM. В этом случае DOM сообщает вам все, что вам нужно знать о его текущем состоянии. Текущее состояние DOM — это то, что пользователи видят в браузере. Вы можете хранить данные о состоянии в самой DOM, пока контроллер не изменит состояние.

Когда происходит событие, контроллер захватывает данные и сообщает, что происходит дальше. Представляет this.showPenguin()PenguinController.prototype.showPenguin = function showPenguin(penguinModelData) {
var penguinViewModel = {
name: penguinModelData.name,
imageUrl: penguinModelData.imageUrl,
size: penguinModelData.size,
favoriteFood: penguinModelData.favoriteFood
};

penguinViewModel.previousIndex = penguinModelData.index 1;
penguinViewModel.nextIndex = penguinModelData.index + 1;

if (penguinModelData.index === 0) {
penguinViewModel.previousIndex = penguinModelData.count 1;
}

if (penguinModelData.index === penguinModelData.count 1) {
penguinViewModel.nextIndex = 0;
}

this.penguinView.render(penguinViewModel);
};

 var PenguinViewMock = function PenguinViewMock() {
  this.calledRenderWith = null;
};

PenguinViewMock.prototype.render = function render(penguinViewModel) {
  this.calledRenderWith = penguinViewModel;
};

// Arrange
var penguinViewMock = new PenguinViewMock();

var controller = new PenguinController(penguinViewMock, null);

var penguinModelData = {
  name: 'Chinstrap',
  imageUrl: 'http://chinstrapl.jpg',
  size: '5.0kg (m), 4.8kg (f)',
  favoriteFood: 'krill',
  index: 2,
  count: 5
};

// Act
controller.showPenguin(penguinModelData);

// Assert
assert.strictEqual(penguinViewMock.calledRenderWith.name, 'Chinstrap');
assert.strictEqual(penguinViewMock.calledRenderWith.imageUrl, 'http://chinstrapl.jpg');
assert.strictEqual(penguinViewMock.calledRenderWith.size, '5.0kg (m), 4.8kg (f)');
assert.strictEqual(penguinViewMock.calledRenderWith.favoriteFood, 'krill');
assert.strictEqual(penguinViewMock.calledRenderWith.previousIndex, 1);
assert.strictEqual(penguinViewMock.calledRenderWith.nextIndex, 3);

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

Вот модульный тест «счастливого пути», показывающий пингвина:

 PenguinViewMock

assert Это позволяет писать модульные тесты и делать утверждения. Утверждение происходит из утверждений Узла , а также доступно в утверждениях Чая . Это позволяет вам писать тесты, которые могут выполняться как на узле, так и в браузере.

Обратите внимание, что контроллер не заботится о деталях реализации. Он использует контракты, предоставляемые представлением, как this.render() Это дисциплина, необходимая для чистого кода. Контроллер может доверять каждому компоненту в том, что он делает. Это добавляет прозрачность, которая делает код читабельным.

Вид

Представление заботится только об элементе DOM и событиях подключения, например:

 var PenguinView = function PenguinView(element) {
  this.element = element;

  this.onClickGetPenguin = null;
};

Когда он изменяет состояние того, что видит пользователь, реализация выглядит так:

 PenguinView.prototype.render = function render(viewModel) {
  this.element.innerHTML = '<h3>' + viewModel.name + '</h3>' +
    '<img class="penguin-image" src="' + viewModel.imageUrl +
      '" alt="' + viewModel.name + '" />' +
    '<p><b>Size:</b> ' + viewModel.size + '</p>' +
    '<p><b>Favorite food:</b> ' + viewModel.favoriteFood + '</p>' +
    '<a id="previousPenguin" class="previous button" href="javascript:void(0);"' +
      ' data-penguin-index="' + viewModel.previousIndex + '">Previous</a> ' +
    '<a id="nextPenguin" class="next button" href="javascript:void(0);"' +
      ' data-penguin-index="' + viewModel.nextIndex + '">Next</a>';

  this.previousIndex = viewModel.previousIndex;
  this.nextIndex = viewModel.nextIndex;

  // Wire up click events, and let the controller handle events
  var previousPenguin = this.element.querySelector('#previousPenguin');
  previousPenguin.addEventListener('click', this.onClickGetPenguin);

  var nextPenguin = this.element.querySelector('#nextPenguin');
  nextPenguin.addEventListener('click', this.onClickGetPenguin);
  nextPenguin.focus();
};

Обратите внимание, что его главная задача — преобразовать данные модели представления в HTML и изменить состояние. Второе — связать события щелчка и позволить контроллеру служить точкой входа. Обработчики событий присоединяются к DOM после изменения состояния. Этот метод обрабатывает управление событиями за один раз.

Чтобы проверить это, мы можем убедиться, что элемент обновляется и изменяет состояние:

 var ElementMock = function ElementMock() {
  this.innerHTML = null;
};

// Stub functions, so we can pass the test
ElementMock.prototype.querySelector = function querySelector() { };
ElementMock.prototype.addEventListener = function addEventListener() { };
ElementMock.prototype.focus = function focus() { };

// Arrange
var elementMock = new ElementMock();

var view = new PenguinView(elementMock);

var viewModel = {
  name: 'Chinstrap',
  imageUrl: 'http://chinstrap1.jpg',
  size: '5.0kg (m), 4.8kg (f)',
  favoriteFood: 'krill',
  previousIndex: 1,
  nextIndex: 2
};

// Act
view.render(viewModel);

// Assert
assert(elementMock.innerHTML.indexOf(viewModel.name) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.imageUrl) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.size) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.favoriteFood) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.previousIndex) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.nextIndex) > 0);

Это о решает все большие проблемы, меняя состояние и проводку событий. Но откуда поступают данные?

Модель

В MVC все заботятся о модели Ajax. Например:

 var PenguinModel = function PenguinModel(XMLHttpRequest) {
  this.XMLHttpRequest = XMLHttpRequest;
};

Обратите внимание, что модуль XMLHttpRequest Это способ сообщить коллегам-программистам, какие компоненты необходимы для этой модели. Если модели требуется больше, чем простой Ajax, вы можете сигнализировать об этом несколькими модулями. Кроме того, с помощью модульных тестов я могу вводить макеты, которые имеют тот же контракт, что и оригинальный модуль.

Время, чтобы получить пингвина на основе индекса:

 PenguinModel.prototype.getPenguin = function getPenguin(index, fn) {
  var oReq = new this.XMLHttpRequest();

  oReq.onload = function onLoad(e) {
    var ajaxResponse = JSON.parse(e.currentTarget.responseText);
    // The index must be an integer type, else this fails
    var penguin = ajaxResponse[index];

    penguin.index = index;
    penguin.count = ajaxResponse.length;

    fn(penguin);
  };

  oReq.open('GET', 'https://codepen.io/beautifulcoder/pen/vmOOLr.js', true);
  oReq.send();
};

Это указывает на конечную точку и получает данные с сервера. Мы можем проверить это, высмеивая данные с помощью модульного теста:

 var LIST_OF_PENGUINS = '[{"name":"Emperor","imageUrl":"http://imageUrl",' +
  '"size":"36.7kg (m), 28.4kg (f)","favoriteFood":"fish and squid"}]';

var XMLHttpRequestMock = function XMLHttpRequestMock() {
  // The system under test must set this, else the test fails
  this.onload = null;
};

XMLHttpRequestMock.prototype.open = function open(method, url, async) {
  // Internal checks, system under test must have a method and url endpoint
  assert(method);
  assert(url);
  // If Ajax is not async, you’re doing it wrong 🙂
  assert.strictEqual(async, true);
};

XMLHttpRequestMock.prototype.send = function send() {
  // Callback on this object simulates an Ajax request
  this.onload({ currentTarget: { responseText: LIST_OF_PENGUINS } });
};

// Arrange
var penguinModel = new PenguinModel(XMLHttpRequestMock);

// Act
penguinModel.getPenguin(0, function onPenguinData(penguinData) {

  // Assert
  assert.strictEqual(penguinData.name, 'Emperor');
  assert(penguinData.imageUrl);
  assert.strictEqual(penguinData.size, '36.7kg (m), 28.4kg (f)');
  assert.strictEqual(penguinData.favoriteFood, 'fish and squid');
  assert.strictEqual(penguinData.index, 0);
  assert.strictEqual(penguinData.count, 1);
});

Как видите, модель заботится только о необработанных данных. Это означает работу с объектами Ajax и JavaScript. Если вы не уверены в Ajax в ванильном JavaScript , есть статья с дополнительной информацией.

Модульные тесты

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

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

Не стесняйтесь осматривать весь набор юнит-тестов . Я думаю, что это поможет вам понять этот шаблон дизайна. Каждый тест для конкретного случая использования; думать об этом как о гранулированных проблемах. Модульные тесты помогут вам обдумать каждую проблему кодирования в отдельности и решить эту проблему. Такое разделение функциональных проблем в MVC оживает с каждым модульным тестом.

Заглядывая вперед

Демонстрация пингвинов имеет только жизнеспособную концепцию, чтобы показать, насколько полезной может быть MVC. Но есть много улучшений, которые можно повторить:

  • Добавить экран со списком всех пингвинов
  • Добавьте события клавиатуры, чтобы вы могли пролистывать пингвинов, добавьте также
  • SVG-диаграмма для визуализации данных, выбора любой точки данных, например размера пингвина

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

Вывод

Я надеюсь, вы понимаете, к чему может привести шаблон проектирования MVC и небольшая дисциплина. Хороший шаблон дизайна не мешает продвижению чистого кода. Он держит вас на задаче, решая проблему только под рукой. Это делает вас более эффективным программистом.

В программировании идея состоит в том, чтобы оставаться рядом с проблемой под рукой, устраняя при этом бесполезность. Искусство программирования раскрывает одну проблему за раз. В MVC это означает одну функциональную проблему за раз.

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

Что мне нравится, так это внимание к одной проблеме. Решение одной проблемы за раз и получение положительных отзывов. Таким образом, вы остаетесь в зоне, продуктивной и свободной от чепухи.

Эта статья была рецензирована Vildan Softic . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!