AngularJS: Розуміння моделі дизайну


147

У контексті цієї публікації Ігор Мінар, керівник AngularJS:

MVC проти MVVM проти MVP . Яка суперечлива тема, на яку багато розробників можуть витрачати години і години, обговорюючи і сперечаючись.

Кілька років AngularJS був ближчим до MVC (а точніше до одного з варіантів на стороні клієнта), але з часом і завдяки багатьом вдосконаленням та ремонту api тепер він наблизився до MVVM - об'єктом $ $ можна вважати ViewModel, який зараз знаходиться прикрашений функцією, яку ми називаємо контролером .

Бути в змозі класифікувати рамку і помістити її в одне з відра MV * має деякі переваги. Це може допомогти розробникам отримати більше комфорту з його apis, полегшивши створення ментальної моделі, яка представляє додаток, який будується з рамкою. Це також може допомогти встановити термінологію, яка використовується розробниками.

Сказавши, я б швидше бачив, як розробники створюють добре продумані програми та керуються розділенням проблем, ніж бачать, як вони витрачають час, сперечаючись про MV * дурниці. І з цієї причини я заявляю, що AngularJS є рамкою MVW - Model-View-Anywhere . Де що б не означало, " що для вас працює ".

Кутовий надає вам велику гнучкість для чіткого відділення логіки презентації від логіки бізнесу та стану презентації. Будь ласка, використовуйте це для підвищення продуктивності та ремонту додатків, а не бурхливих дискусій про речі, які в кінці дня не мають великого значення.

Чи є якісь рекомендації чи вказівки щодо впровадження дизайнерської моделі AngularJS MVW (Model-View-Wever) у додатках на стороні клієнта?


прихильний за ... ніж бачити, як вони витрачають час, сперечаючись про MV * дурниці.
Ширгілл Фархан

1
Вам не потрібно Angular, щоб слідувати схемі дизайну класу слів.
корисноБег

Відповіді:


223

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


Контролер

  • Контролер повинен бути лише прошарком між моделлю та видом. Постарайтеся зробити це якомога тонкішим .

  • Настійно рекомендується уникати ділової логіки в контролері. Його слід перенести на модель.

  • Контролер може спілкуватися з іншими контролерами, використовуючи виклик методу (можливо, коли діти хочуть спілкуватися з батьком) або $ emit , $ Broadcast та $ на методах. Повідомлення, що передаються та транслюються, повинні бути зведені до мінімуму.

  • Контролер не повинен дбати про презентацію чи маніпуляції з DOM.

  • Намагайтеся уникати вкладених контролерів . У цьому випадку батьківський контролер інтерпретується як модель. Натомість введіть моделі як спільні служби.

  • Область контролера повинна використовуватися для моделі прив'язки з видом і
    інкапсуляцією представленої моделі, як для моделі дизайну презентаційної моделі .


Область застосування

Розглядайте сферу дії як шаблони лише для читання в шаблонах і лише для запису в контролерах . Метою сфери застосування є посилання на модель, а не на модель.

Виконуючи двонаправлене прив'язування (ng-модель), переконайтеся, що ви не прив'язуєте безпосередньо до властивостей області.


Модель

Модель в 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
    // ...
  };
});

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

Як правило, намагайтеся уникати подібних ситуацій і правильно конструюйте свої моделі, щоб вони могли бути введені в контролер і використані на ваш погляд.

Зокрема, деякі методи вимагають глобальної доступності всередині програми. Щоб зробити це можливим, ви можете визначити властивість ' common ' у $ rootScope та прив’язати його до commonModel під час завантаження програми:

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

Усі ваші глобальні методи житимуть у межах " загальної " властивості Це якийсь простір імен .

Але не визначайте жодних методів безпосередньо у вашому $ rootScope . Це може призвести до несподіваної поведінки при використанні директиви ngModel у межах вашої області перегляду, як правило, засмічує сферу вашої діяльності та призводить до вирішення проблем, що вирішують методи застосування.


Ресурс

Ресурс дозволяє взаємодіяти з різними джерелами даних .

Потрібно реалізовуватись за принципом єдиної відповідальності .

Зокрема, це проксі для багаторазового використання для кінцевих точок HTTP / JSON.

Ресурси вводяться в моделі і забезпечують можливість надсилання / отримання даних.

Реалізація ресурсів

Фабрика, яка створює ресурсний об'єкт, що дозволяє взаємодіяти з джерелами даних на сервері RESTful.

Об'єкт, що повертається, має методи дії, які забезпечують поведінку на високому рівні без необхідності взаємодії з послугою $ http низького рівня.


Послуги

І модель, і ресурс - це послуги .

Служби - це не асоційовані, нещільно поєднані одиниці функціональності, які є самостійними.

Послуги - це особливість, яку 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/isissue/109


5
У мене є одна стурбованість з приводу: "Настійно рекомендується уникати ділової логіки в контролері. Її слід перенести на модель". Однак з офіційної документації ви можете прочитати: "Взагалі контролер не повинен намагатися робити занадто багато. Він повинен містити лише ділову логіку, необхідну для одного перегляду." Ми говоримо про одне й те саме?
op1ekun

3
Я б сказав, розглядайте контролер як модель перегляду.
Артем Платонов

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) - це все добре і просто в одному агенті, але архітектура сервер-клієнт для всіх цілей є двоагентною системою, і люди часто настільки одержимі цими зразками, що вони забувають, що проблема, що існує, є набагато складнішою. Намагаючись дотримуватися цих принципів, вони фактично закінчуються хибною архітектурою.

Давайте зробимо це потроху.

Вказівки

Перегляди

У кутовому контексті погляд є DOM. Вказівки:

Зробіть:

  • Нинішня змінна область (лише для читання).
  • Викликайте контролер для дій.

Не:

  • Покладіть будь-яку логіку.

Як заманливо, коротке і нешкідливе це виглядає:

ng-click="collapsed = !collapsed"

Це в значній мірі означає будь-якого розробника, що тепер, щоб зрозуміти, як працює система, їм потрібно перевіряти як файли Javascript, так і HTML.

Контролери

Зробіть:

  • Прив’яжіть погляд до «моделі», розмістивши дані про область застосування.
  • Реагуйте на дії користувача.
  • Займайтеся логікою презентації.

Не:

  • Займайтеся будь-якою логікою бізнесу.

Причина останнього керівництва полягає в тому, що контролери - це сестри поглядів, а не сутності; ні вони не можуть бути використані повторно.

Ви можете стверджувати, що директиви можуть використовуватись повторно, але директиви теж є сестрами поглядів (DOM) - вони ніколи не мали на меті відповідати юридичним особам.

Звичайно, іноді погляди представляють сутність, але це досить конкретний випадок.

Іншими словами, контролери повинні зосередитися на презентації - якщо ви введете ділову логіку, ви не тільки, швидше за все, отримаєте завищений, мало керований контролер, але ви також порушите принцип розмежування проблеми .

Таким чином, контролери в кутових кузовах дійсно більше представлені у презентаційній моделі або MVVM .

І так, якщо контролери не повинні займатися діловою логікою, хто повинен?

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

Модель вашого клієнта часто часткова і несвіжа

Якщо ви не пишете офлайн-веб-додаток або надзвичайно простий додаток (мало об’єктів), великою вірогідністю може бути ваша клієнтська модель:

  • Часткове
    • Або у нього немає всіх сутностей (як, наприклад, у випадку пагінації)
    • Або він не має всіх даних (як, наприклад, у випадку пагінації)
  • Старе - Якщо в системі є більше одного користувача, в будь-який момент ви не можете бути впевнені, що модель, яку має клієнт, така ж, як та, яку має сервер.

Справжня модель повинна зберігатися

У традиційних MCV модель є єдиним, що зберігається . Щоразу, коли ми говоримо про моделі, вони повинні зберігатися в якийсь момент. Ваш клієнт може керувати моделями за власним бажанням, але поки успішно завершено перехід на сервер, робота не виконана.

Наслідки

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

Таким чином, можливо, в контексті клієнта доцільно використовувати малі регістри M- значить, це дійсно mVC , mVP та mVVm . Велике M- для сервера.

Бізнес-логіка

Мабуть, однією з найважливіших концепцій бізнес-моделей є те, що ви можете поділити їх на 2 типи (я опускаю третій погляд-діловий, оскільки це історія для іншого дня):

  • Логіка домену - також бізнес-правила підприємства Enterprise , логіка, яка не залежить від додатків. Наприклад, наведіть модель із властивостями firstNameта sirNameвластивості, подібне геттер getFullName()може вважатися незалежним від програми.
  • Логіка програми - він же, як бізнес-правила , що стосуються програми. Наприклад, перевірка помилок та поводження з ними.

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

Залишається питання - куди ви їх кидаєте в кутовий додаток?

3-х шарова архітектура

Усі ці рамки МВТ використовують 3 шари:

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

Але це стосується двох основних питань, що стосуються клієнтів:

  • Модель часткова, застаріла і не зберігається.
  • Немає місця для логіки програми.

Альтернативою цій стратегії є чотиришарова стратегія :

4 кола, від внутрішнього до зовнішнього - Правила підприємницької діяльності, Правила бізнес-додатків, Адаптери інтерфейсу, Рамки та драйвери

Справжньою угодою тут є рівень правил роботи з додатками (Use Use), який часто не відповідає клієнтам.

Цей шар реалізований інтеракторами (дядько Боб), що в значній мірі називає Мартін Фаулер службовим рівнем сценарію експлуатації .

Конкретний приклад

Розглянемо наступний веб-додаток:

  • У додатку відображається хворобливий список користувачів.
  • Користувач натискає "Додати користувача".
  • Модель відкривається з формою для заповнення даних про користувача.
  • Користувач заповнює форму і натискає надіслати.

Зараз має відбутися кілька речей:

  • Форма повинна бути підтверджена клієнтом.
  • Запит надсилається серверу.
  • Помилка обробляється, якщо така є.
  • Список користувачів може або не може (через пагінацію) потребує оновлення.

Куди ми все це кидаємо?

Якщо ваша архітектура включає контролер, який дзвонить $resource, все це відбуватиметься в межах контролера. Але є краща стратегія.

Пропоноване рішення

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

4 поля - DOM вказує на контролер, який вказує на логіку програми, яка вказує на $ ресурс

Отже, ми додаємо шар між контролером до ресурсу $, цей шар (дозволяє називати його інтерактором ):

  • Це послуга . У випадку користувачів це може бути викликано UserInteractor.
  • Він надає методи, що відповідають випадкам використання , інкапсулюючи логіку програми .
  • Він контролює запити, зроблені до сервера. Замість того, щоб контролер викликав $ ресурс з параметрами вільної форми, цей рівень забезпечує, що запити, спрямовані на сервер, повертають дані, щодо яких може діяти логіка домену.
  • Він прикрашає повернуту структуру даних прототипом логіки домену .

І так, з вимогами конкретного прикладу вище:

  • Користувач натискає "Додати користувача".
  • Контролер запитує у інтерактора про порожню модель користувача, оздоблений методом бізнес-логіки, як validate()
  • Після подання контролер викликає validate()метод моделі .
  • Якщо не вдалося, контролер обробляє помилку.
  • У разі успіху контролер викликає інтерактор createUser()
  • Інтерактор викликає $ ресурс
  • Після відповіді інтерактор делегує будь-які помилки контролеру, який їх обробляє.
  • Після успішної відповіді, інтерактор гарантує, що при необхідності список користувачів оновлюється.

Таким чином, AngularJS визначається MVW (де W - для будь-якого), оскільки я можу вибрати контролер (з усією діловою логікою в ньому) або модель перегляду / презентатор (без ділової логіки, але лише якийсь код для заповнення подання) з BL окрема послуга? Маю рацію?
BAD_SEED

Найкраща відповідь. Чи є у вас справжній приклад на GitHub 4-шарового кутового додатка?
RPallas

1
@RPallas, Ні, я не хочу (хотілося б, щоб у мене був час на це). В даний час ми випробовуємо архітектуру, де "логіка програми" є лише межовим взаємодієм; роздільну здатність між ним та контролером та модель перегляду, яка має певну логіку перегляду. Ми все ще експериментуємо, тому не 100% плюсів чи мінусів. Але після закінчення я сподіваюся десь написати блог.
Іжакі

1
@heringer В основному ми представили моделі - OOP-конструкції, які представляють доменні сутності. Саме ці моделі спілкуються з ресурсами, а не з контролерами. Вони інкапсулюють логіку домену. Контролери називають моделі, які, в свою чергу, викликають ресурси.
Іжакі

1
@ alex440 Ні. Хоча минуло вже два місяці, коли серйозна публікація блогу на цю тему лежить у кінчиках моїх пальців. Приходить Xmas - можливо, тоді.
Іжакі

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 також може називатися в'яжучим (у кутовому випадку це може бути $ обхват). Модель -> Як ми знаємо, модель в кутовій формі може бути просто простими старими об'єктами JS або даними в нашому додатку

Перегляд -> вигляд у angularJS - це HTML, який був розібраний та скомпільований angularJS, застосовуючи директиви чи інструкції чи прив'язки. Основна суть тут у кутовому введенні - це не просто звичайна HTML-рядок (innerHTML), а це є DOM, створений браузером.

ViewModel -> ViewModel насправді є зв'язувачем / мостом між вашим оглядом та моделлю у кутовому випадку JS, це $ range, щоб ініціалізувати та збільшити область застосування $, яку ми використовуємо Controller.

Якщо я хочу узагальнити відповідь: У застосуванні angularJS $ range має посилання на дані, Controller контролює поведінку, а View обробляє макет, взаємодіючи з контролером, щоб поводитися відповідно.


-1

Щоб чітко поставитись до цього питання, Angular використовує різні шаблони дизайну, з якими ми вже стикалися в звичайному програмуванні. 1) Коли ми реєструємо наші контролери чи директиви, фабрику, послуги тощо стосовно нашого модуля. Тут він приховує дані з глобального простору. Що таке шаблон модуля . 2) Коли angular використовує свою брудну перевірку для порівняння змінних областей, тут використовується шаблон Observer . 3) Усі контрольні області батьків у наших контролерах використовують прототипний шаблон. 4) У випадку ін'єкції послуг використовується Factory Pattern .

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.