AngularJS: понимание шаблона проектирования

147

В контексте этого поста Игорь Минар, ведущий AngularJS:

MVC против MVVM против MVP . Что за спорная тема, о которой многие разработчики могут часами спорить и спорить.

В течение нескольких лет AngularJS был ближе к MVC (или, скорее, к одному из его вариантов на стороне клиента), но со временем и благодаря множеству рефакторингов и улучшений API теперь он ближе к MVVM - объект $ scope можно рассматривать как модель ViewModel, которая сейчас украшен функцией, которую мы называем контроллером .

Возможность классифицировать фреймворк и поместить его в одну из групп MV * имеет некоторые преимущества. Он может помочь разработчикам освоиться с его API-интерфейсом, упрощая создание ментальной модели, представляющей приложение, которое создается с помощью фреймворка. Это также может помочь установить терминологию, которая используется разработчиками.

Сказав это, я бы предпочел, чтобы разработчики создавали офигенные приложения, которые были бы хорошо спроектированы и следили за разделением интересов, чем видел, как они тратят время на споры о бессмысленности MV *. И по этой причине, я заявляю AngularJS быть MVW рамки - Model-View-Безотносительно . Где Что бы ни означало « все, что работает для вас ».

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

Существуют ли какие-либо рекомендации или рекомендации по внедрению шаблона проектирования AngularJS MVW (модель-вид-что-либо) в клиентские приложения?

Артем Платонов
источник
голосовал за ... чем видел, как они тратят время на споры о MV * ерунде.
Ширгилл Фархан
1
Вам не нужно, чтобы Angular следовал шаблону дизайна словесного класса.
полезноBee

Ответы:

223

Благодаря огромному количеству ценных источников я получил несколько общих рекомендаций по реализации компонентов в приложениях AngularJS:


контроллер

  • Контроллер должен быть просто прослойкой между моделью и видом. Постарайтесь сделать его максимально тонким .

  • Настоятельно рекомендуется избегать бизнес-логики в контроллере. Следует перенести в модель.

  • Контроллер может связываться с другими контроллерами, используя вызов метода (возможно, когда дети хотят общаться с родителем) или методы $ emit , $ broadcast и $ on . Передаваемые и передаваемые сообщения должны быть сведены к минимуму.

  • Контроллер не должен заботиться о представлении или манипулировании DOM.

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

  • Область применения контроллера должен использоваться для связывания модели с учетом и
    герметизирующего View Model , как для представления модели шаблона проектирования.


Объем

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

При выполнении двунаправленной привязки (ng-model) убедитесь, что вы не привязываете напрямую к свойствам области.


Модель

Модель в AngularJS является одноэлементной, определяемой сервисом .

Модель предоставляет отличный способ разделения данных и отображения.

Модели являются основными кандидатами для модульного тестирования, поскольку они обычно имеют ровно одну зависимость (некоторую форму генератора событий, в общем случае $ rootScope ) и содержат хорошо проверяемую логику домена .

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

  • Модель должна инкапсулировать данные вашего приложения и предоставлять API для доступа к этим данным и манипулирования ими.

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

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

  • Модель может использовать методы более общих глобальных моделей, которые являются общими для всего приложения.

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

  • Старайтесь избегать использования слушателей событий в моделях. Это затрудняет их тестирование и в целом убивает модели с точки зрения принципа единой ответственности.

Реализация модели

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

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

Пример :

angular.module('search')
.factory( 'searchModel', ['searchResource', function (searchResource) {

  var itemsPerPage = 10,
  currentPage = 1,
  totalPages = 0,
  allLoaded = false,
  searchQuery;

  function init(params) {
    itemsPerPage = params.itemsPerPage || itemsPerPage;
    searchQuery = params.substring || searchQuery;
  }

  function findItems(page, queryParams) {
    searchQuery = queryParams.substring || searchQuery;

    return searchResource.fetch(searchQuery, page, itemsPerPage).then( function (results) {
      totalPages = results.totalPages;
      currentPage = results.currentPage;
      allLoaded = totalPages <= currentPage;

      return results.list
    });
  }

  function findNext() {
    return findItems(currentPage + 1);
  }

  function isAllLoaded() {
    return allLoaded;
  }

  // return public model API  
  return {
    /**
     * @param {Object} params
     */
    init: init,

    /**
     * @param {Number} page
     * @param {Object} queryParams
     * @return {Object} promise
     */
    find: findItems,

    /**
     * @return {Boolean}
     */
    allLoaded: isAllLoaded,

    /**
     * @return {Object} promise
     */
    findNext: findNext
  };
});

Создание новых экземпляров

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

Лучший способ сделать то же самое - использовать фабрику как API для возврата коллекции объектов с прикрепленными к ним методами getter и setter.

angular.module('car')
 .factory( 'carModel', ['carResource', function (carResource) {

  function Car(data) {
    angular.extend(this, data);
  }

  Car.prototype = {
    save: function () {
      // TODO: strip irrelevant fields
      var carData = //...
      return carResource.save(carData);
    }
  };

  function getCarById ( id ) {
    return carResource.getById(id).then(function (data) {
      return new Car(data);
    });
  }

  // the public API
  return {
    // ...
    findById: getCarById
    // ...
  };
});

Глобальная модель

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

В частном случае некоторые методы требуют глобальной доступности в приложении. Чтобы сделать это возможным, вы можете определить « общее » свойство в $ rootScope и связать его с commonModel во время начальной загрузки приложения:

angular.module('app', ['app.common'])
.config(...)
.run(['$rootScope', 'commonModel', function ($rootScope, commonModel) {
  $rootScope.common = 'commonModel';
}]);

Все ваши глобальные методы будут жить в рамках « общего » свойства. Это какое-то пространство имен .

Но не определяйте какие-либо методы непосредственно в вашем $ rootScope . Это может привести к неожиданному поведению при использовании с директивой ngModel в вашей области видимости, как правило, засоряет вашу область и приводит к тому, что методы области переопределяют проблемы.


Ресурс

Ресурс позволяет вам взаимодействовать с различными источниками данных .

Должно быть реализовано с использованием принципа единой ответственности .

В частном случае это повторно используемый прокси для конечных точек HTTP / JSON.

Ресурсы внедряются в модели и предоставляют возможность отправлять / извлекать данные.

Реализация ресурсов

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

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


Сервисы

И модель, и ресурс являются услугами .

Сервисы - это несвязанные, слабо связанные блоки функциональности, которые являются автономными.

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

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

Angular поставляется с различными видами услуг. Каждый со своими вариантами использования. Пожалуйста, прочитайте Понимание типов услуг для деталей.

Попробуйте рассмотреть основные принципы архитектуры сервиса в вашем приложении.

В целом, согласно Глоссарию веб-сервисов :

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


Клиентская структура

В целом клиентская часть приложения разбита на модули . Каждый модуль должен быть тестируемым как единое целое.

Попробуйте определить модули в зависимости от функции / функциональности или вида , а не по типу. Смотрите презентацию Миско для деталей.

Компоненты модуля могут быть условно сгруппированы по типам, таким как контроллеры, модели, представления, фильтры, директивы и т. Д.

Но сам модуль остается многоразовым , передаваемым и тестируемым .

Разработчикам также намного проще найти некоторые части кода и все его зависимости.

Пожалуйста, обратитесь к организации кода в больших AngularJS и JavaScript приложений для деталей.

Пример структурирования папок :

|-- src/
|   |-- app/
|   |   |-- app.js
|   |   |-- home/
|   |   |   |-- home.js
|   |   |   |-- homeCtrl.js
|   |   |   |-- home.spec.js
|   |   |   |-- home.tpl.html
|   |   |   |-- home.less
|   |   |-- user/
|   |   |   |-- user.js
|   |   |   |-- userCtrl.js
|   |   |   |-- userModel.js
|   |   |   |-- userResource.js
|   |   |   |-- user.spec.js
|   |   |   |-- user.tpl.html
|   |   |   |-- user.less
|   |   |   |-- create/
|   |   |   |   |-- create.js
|   |   |   |   |-- createCtrl.js
|   |   |   |   |-- create.tpl.html
|   |-- common/
|   |   |-- authentication/
|   |   |   |-- authentication.js
|   |   |   |-- authenticationModel.js
|   |   |   |-- authenticationService.js
|   |-- assets/
|   |   |-- images/
|   |   |   |-- logo.png
|   |   |   |-- user/
|   |   |   |   |-- user-icon.png
|   |   |   |   |-- user-default-avatar.png
|   |-- index.html

Хороший пример структурирования угловых приложений реализован в angular-app - https://github.com/angular-app/angular-app/tree/master/client/src

Это также учитывается современными генераторами приложений - https://github.com/yeoman/generator-angular/issues/109

Артем Платонов
источник
5
У меня есть одна проблема: «Настоятельно рекомендуется избегать бизнес-логики в контроллере. Ее следует перенести в модель». Однако из официальной документации вы можете прочитать: «В общем, Контроллер не должен пытаться делать слишком много. Он должен содержать только бизнес-логику, необходимую для одного представления». Мы говорим об одном и том же?
op1ekun
3
Я бы сказал - относитесь к Controller как к View Model.
Артем Платонов
1
+1. Несколько отличных советов здесь! 2. К сожалению, пример searchModelне следует совету по повторному использованию. Было бы лучше импортировать константы через constantсервис. 3. Любое объяснение, что здесь подразумевается ?:Try to avoid having a factory that returns a new able function
Дмитрий Зайцев
1
Кроме того, перезапись prototypeсвойства объекта нарушает наследование, вместо этого можно использоватьCar.prototype.save = ...
Дмитрий Зайцев
2
@ChristianAichinger, это о природе цепочки прототипов JavaScript, которая заставляет вас либо использовать objectв вашем выражении двустороннего связывания, чтобы убедиться, что вы пишете точное свойство или setterфункцию. В случае использования прямого свойства вашей области ( без точки ) вы рискуете скрыть желаемое целевое свойство с недавно созданным в ближайшей верхней области в цепочке прототипов при записи в него. Это лучше объяснить в презентации Миско
Артем Платонов
46

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

MVC и его производные (MVP, PM, MVVM) все хороши и хороши в одном агенте, но архитектура сервер-клиент для всех целей является системой с двумя агентами, и люди часто настолько одержимы этими шаблонами, что забывают, что проблема под рукой гораздо сложнее. Пытаясь придерживаться этих принципов, они фактически получают ошибочную архитектуру.

Давайте сделаем это по крупицам.

Руководящие принципы

Просмотры

В контексте Angular представление является DOM. Руководящие принципы:

Делать:

  • Представить переменную области (только для чтения).
  • Вызовите контроллер для действий.

Не рекомендуется:

  • Поставь любую логику.

Как заманчиво, коротко и безобидно это выглядит:

ng-click="collapsed = !collapsed"

Любой разработчик в значительной степени показывает, что теперь, чтобы понять, как работает система, им нужно проверять как файлы Javascript, так и файлы HTML.

Контроллеры

Делать:

  • Свяжите представление с «моделью», поместив данные в область.
  • Отвечать на действия пользователя.
  • Разобраться с логикой представления.

Не рекомендуется:

  • Разобраться с любой бизнес-логикой.

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

Можно утверждать, что директивы можно использовать повторно, но директивы также являются сестрами представлений (DOM) - они никогда не предназначались для соответствия сущностям.

Конечно, иногда представления представляют сущности, но это довольно специфический случай.

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

Таким образом, контроллеры в Angular - это скорее Презентационная модель или MVVM .

И так, если контроллеры не должны иметь дело с бизнес-логикой, кто должен?

Что такое модель?

Ваша модель клиента часто является частичной и устаревшей

Если вы не пишете автономное веб-приложение или ужасно простое приложение (несколько сущностей), ваша модель клиента, скорее всего, будет:

  • частичный
    • Либо он не имеет всех сущностей (как в случае нумерации страниц)
    • Или он не имеет всех данных (как в случае нумерации страниц)
  • Устаревшие - если в системе более одного пользователя, в любой момент вы не можете быть уверены, что модель, которую держит клиент, совпадает с моделью, которую держит сервер.

Настоящая модель должна сохраняться

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

последствия

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

Поэтому в контексте клиента целесообразно использовать строчные буквы M- так что это действительно mVC , mVP и mVVm . Большое Mдля сервера.

Бизнес логика

Возможно, одна из самых важных концепций о бизнес-моделях заключается в том, что вы можете разделить их на 2 типа (я опускаю третий вид бизнес-модели, поскольку это история для другого дня):

  • Доменная логика - бизнес-правила предприятия , логика, независимая от приложений. Так , например, дать модель с firstNameи sirNameсвойствами, геттер , как getFullName()можно считать приложение-независимым.
  • Логика приложения - так называемые бизнес-правила приложения , которые зависят от приложения. Например, проверка ошибок и обработка.

Важно подчеркнуть, что оба они в контексте клиента не являются «реальной» бизнес-логикой - они имеют дело только с той частью, которая важна для клиента. Прикладная логика (не доменная логика) должна нести ответственность за облегчение связи с сервером и большую часть взаимодействия с пользователем; в то время как логика предметной области в основном мелкая, специфичная для сущности и управляемая представлением.

Вопрос все еще остается - куда вы кидаете их в угловое приложение?

3 против 4 уровня архитектуры

Все эти MVW фреймворки используют 3 слоя:

Три круга  Внутренний - модель, средний - контроллер, внешний - вид

Но есть две фундаментальные проблемы с этим, когда речь идет о клиентах:

  • Модель является частичной, несвежей и не сохраняется.
  • Нет места для размещения логики приложения.

Альтернативой этой стратегии является четырехуровневая стратегия :

4 круга, от внутреннего к внешнему - бизнес-правила предприятия, бизнес-правила приложений, адаптеры интерфейса, фреймворки и драйверы

Реальная сделка здесь - это слой бизнес-правил приложения (Варианты использования), который часто не работает для клиентов.

Этот уровень реализуется с помощью интеракторов (дядя Боб), который Мартин Фаулер называет уровнем обслуживания сценариев операций .

Конкретный пример

Рассмотрим следующее веб-приложение:

  • Приложение отображает нумерованный список пользователей.
  • Пользователь нажимает «Добавить пользователя».
  • Модель открывается с формой для заполнения пользовательских данных.
  • Пользователь заполняет форму и нажимает кнопку Отправить.

Несколько вещей должно произойти сейчас:

  • Форма должна быть подтверждена клиентом.
  • Запрос должен быть отправлен на сервер.
  • Ошибка должна быть обработана, если она есть.
  • Список пользователей может или не может (из-за нумерации страниц) нуждается в обновлении.

Куда мы все это бросаем?

Если ваша архитектура включает контроллер, который вызывает $resource , все это будет происходить внутри контроллера. Но есть лучшая стратегия.

Предлагаемое решение

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

4 поля - DOM указывает на контроллер, который указывает на логику приложения, которая указывает на $ resource

Итак, мы добавляем слой между контроллером в $ resource, этот слой (давайте назовем его интерактором ):

  • Это сервис . В случае пользователей это можно назвать UserInteractor.
  • Он предоставляет методы, соответствующие сценариям использования , инкапсулируя логику приложения .
  • Он контролирует запросы к серверу. Вместо того, чтобы контроллер вызывал $ resource с параметрами свободной формы, этот уровень гарантирует, что запросы к серверу возвращают данные, на которые может воздействовать логика домена.
  • Он украшает возвращенную структуру данных прототипом логики предметной области .

И так, с требованиями конкретного примера выше:

  • Пользователь нажимает «Добавить пользователя».
  • Контроллер запрашивает у интерактора пустую пользовательскую модель, которая украшена методом бизнес-логики, например validate()
  • После отправки контроллер вызывает validate()метод модели .
  • В случае неудачи контроллер обрабатывает ошибку.
  • В случае успеха контроллер вызывает интерактор с createUser()
  • Интерактор вызывает $ ресурс
  • После ответа интерактор передает любые ошибки контроллеру, который их обрабатывает.
  • После успешного ответа интерактор гарантирует, что при необходимости список пользователей обновляется.
Izhaki
источник
Таким образом, AngularJS определяется MVW (где W для чего угодно), так как я могу выбрать контроллер (со всей бизнес-логикой в ​​нем) или View Model / Presenter (без бизнес-логики, но только некоторый код для заполнения представления) с BL в отдельный сервис? Я прав?
BAD_SEED
Лучший ответ. У вас есть реальный пример на GitHub четырехслойного углового приложения?
RPallas
1
@RPallas, нет, не знаю (жаль, что у меня не было времени для этого). В настоящее время мы испытываем архитектуру, в которой «логика приложения» является просто граничным интерактором; преобразователь между ним и контроллером и модель представления, которая имеет некоторую логику представления. Мы все еще экспериментируем, поэтому не 100% плюсов или минусов. Но однажды я надеюсь написать блог где-нибудь.
Изаки,
1
@heringer По сути, мы представили модели - конструкции ООП, которые представляют сущности домена. Именно эти модели взаимодействуют с ресурсами, а не с контроллерами. Они инкапсулируют доменную логику. Контроллеры вызывают модели, которые в свою очередь вызывают ресурсы.
Изаки
1
@ alex440 Нет. Хотя прошло уже два месяца, как серьезный пост в блоге на эту тему у меня под рукой. Рождество идет - возможно, тогда.
Ижаки
5

Незначительная проблема по сравнению с замечательными советами в ответе Артема, но с точки зрения читабельности кода, я обнаружил, что лучше всего определить API полностью внутри returnобъекта, чтобы свести к минимуму переходы вперед и назад в коде для просмотра, где будут определены переменные:

angular.module('myModule', [])
// or .constant instead of .value
.value('myConfig', {
  var1: value1,
  var2: value2
  ...
})
.factory('myFactory', function(myConfig) {
  ...preliminary work with myConfig...
  return {
    // comments
    myAPIproperty1: ...,
    ...
    myAPImethod1: function(arg1, ...) {
    ...
    }
  }
});

Если returnобъект выглядит «слишком людным», это признак того, что Сервис делает слишком много.

Дмитрий Зайцев
источник
0

AngularJS не реализует MVC традиционным способом, скорее он реализует нечто более близкое к MVVM (Model-View-ViewModel), ViewModel также может называться связывателем (в угловом случае это может быть $ scope). Модель -> Как мы знаем, угловая модель может быть просто старыми объектами JS или данными в нашем приложении.

Представление -> представление в angularJS - это HTML-код, который был проанализирован и скомпилирован angularJS с применением директив, инструкций или привязок. Главное здесь в угловом вводе - это не просто простая строка HTML (innerHTML), а DOM создан браузером

ViewModel -> ViewModel на самом деле является связующим звеном / мостом между вашим представлением и моделью в случае angularJS, это $ scope, чтобы инициализировать и дополнить $ scope, которую мы используем Controller.

Если я хочу обобщить ответ: в приложении angularJS $ scope имеет ссылку на данные, Controller управляет поведением, а View обрабатывает макет, взаимодействуя с контроллером, чтобы вести себя соответствующим образом.

Ashutosh
источник
-1

Чтобы быть ясным в этом вопросе, Angular использует различные шаблоны проектирования, с которыми мы уже сталкивались в нашем обычном программировании. 1) Когда мы регистрируем наши контроллеры или директивы, фабрики, службы и т. Д. В отношении нашего модуля. Здесь скрываются данные из глобального пространства. Что является модульным шаблоном . 2) Когда angular использует свою грязную проверку для сравнения переменных области видимости, здесь он использует Observer Pattern . 3) Все родительские дочерние области в наших контроллерах используют шаблон Prototypal. 4) В случае внедрения сервисов он использует Factory Pattern .

В целом он использует различные известные шаблоны проектирования для решения проблем.

Навин Редди
источник