Где разместить модель данных и поведения? [TL; др; Использовать услуги]
Я работаю с AngularJS для моего последнего проекта. В документации и руководствах все данные модели помещаются в область действия контроллера. Я понимаю, что это должно быть там, чтобы быть доступным для контроллера и, следовательно, в соответствующих представлениях.
Однако я не думаю, что модель должна быть реализована там. Это может быть сложно и иметь частные атрибуты, например. Кроме того, можно использовать его в другом контексте / приложении. Помещение всего в контроллер полностью нарушает схему MVC.
То же самое относится и к поведению любой модели. Если бы я использовал архитектуру DCI и отделил поведение от модели данных, мне пришлось бы вводить дополнительные объекты для хранения поведения. Это будет сделано путем введения ролей и контекстов.
Конечно, данные модели и поведение могут быть реализованы с помощью простых объектов JavaScript или любого шаблона "класса". Но каков будет AngularJS способ сделать это? Пользуетесь услугами?
Итак, все сводится к этому вопросу:
Как вы реализуете модели, отделенные от контроллера, следуя рекомендациям AngularJS?
8 ответов
Вы должны использовать сервисы, если вы хотите что-то, что может использоваться несколькими контроллерами. Вот простой надуманный пример:
myApp.factory('ListService', function() {
var ListService = {};
var list = [];
ListService.getItem = function(index) { return list[index]; }
ListService.addItem = function(item) { list.push(item); }
ListService.removeItem = function(item) { list.splice(list.indexOf(item), 1) }
ListService.size = function() { return list.length; }
return ListService;
});
function Ctrl1($scope, ListService) {
//Can add/remove/get items from shared list
}
function Ctrl2($scope, ListService) {
//Can add/remove/get items from shared list
}
В настоящее время я пробую этот шаблон, который, хотя и не DCI, обеспечивает классическое разделение службы / модели (со службами для общения с веб-службами (также называемыми моделью CRUD) и моделью, определяющей свойства и методы объекта).
Обратите внимание, что я использую этот шаблон только тогда, когда объекту модели нужны методы, работающие с его собственными свойствами, которые я, вероятно, буду использовать везде (например, улучшенные методы получения / установки). Я не рекомендую делать это для каждой службы систематически.
РЕДАКТИРОВАТЬ: Раньше я думал, что этот шаблон будет идти против мантры "Угловая модель - это простой старый объект javascript", но теперь мне кажется, что этот шаблон идеально подходит.
РЕДАКТИРОВАТЬ (2): Чтобы быть еще яснее, я использую класс Model только для учета простых методов получения / установки (например, для использования в шаблонах представления). Для большой бизнес-логики я рекомендую использовать отдельные службы, которые "знают" о модели, но хранятся отдельно от них и включают только бизнес-логику. Назовите его сервисным уровнем "бизнес-эксперт", если хотите
service / ElementServices.js (обратите внимание, как элемент добавляется в декларации)
MyApp.service('ElementServices', function($http, $q, Element)
{
this.getById = function(id)
{
return $http.get('/element/' + id).then(
function(response)
{
//this is where the Element model is used
return new Element(response.data);
},
function(response)
{
return $q.reject(response.data.error);
}
);
};
... other CRUD methods
}
model / Element.js (с помощью angularjs Factory, созданной для создания объектов)
MyApp.factory('Element', function()
{
var Element = function(data) {
//set defaults properties and functions
angular.extend(this, {
id:null,
collection1:[],
collection2:[],
status:'NEW',
//... other properties
//dummy isNew function that would work on two properties to harden code
isNew:function(){
return (this.status=='NEW' || this.id == null);
}
});
angular.extend(this, data);
};
return Element;
});
Документация Angularjs четко гласит:
В отличие от многих других фреймворков, Angular не устанавливает никаких ограничений или требований к модели. Нет классов для наследования или специальных методов доступа для доступа или изменения модели. Модель может быть примитивом, хэшем объекта или полным типом объекта. Короче говоря, модель представляет собой простой объект JavaScript.
Так что это зависит от вас, как объявить модель. Это простой объект Javascript.
Лично я не буду использовать Angular Services, поскольку они должны были вести себя как одноэлементные объекты, которые вы можете использовать, например, для сохранения глобальных состояний в вашем приложении.
DCI - это парадигма, и поэтому нет никакого угловатого способа сделать это, либо язык поддерживает DCI, либо нет. JS довольно хорошо поддерживает DCI, если вы хотите использовать преобразование исходного кода, и с некоторыми недостатками, если вы этого не делаете. Опять же, DCI не имеет ничего общего с внедрением зависимостей, как, скажем, класс C# и определенно не является сервисом. Таким образом, лучший способ сделать DCI с помощью angulusJS - это сделать DCI способом JS, который довольно близок к тому, как в первую очередь формулируется DCI. Если вы не выполните преобразование исходного кода, вы не сможете сделать это полностью, так как методы роли будут частью объекта даже вне контекста, но это, как правило, проблема с DCI на основе внедрения метода. Если вы посмотрите на http://fulloo.info/ авторитетный сайт для DCI, вы можете взглянуть на реализации ruby, которые также используют внедрение метода, или вы можете посмотреть здесь для получения дополнительной информации о DCI. Это в основном примеры RUby, но материал DCI не зависит от этого. Один из ключей к DCI заключается в том, что то, что делает система, отделено от того, чем она является. Таким образом, объект данных довольно тупой, но однажды связанный с ролью в методах роли контекста делает доступным определенное поведение. Роль - это просто идентификатор, не более того, при доступе к объекту через этот идентификатор доступны методы роли. Там нет роли объекта / класса. С внедрением метода область действия ролевых методов не совсем соответствует описанию, но близка. Примером контекста в JS может быть
function transfer(source,destination){
source.transfer = function(amount){
source.withdraw(amount);
source.log("withdrew " + amount);
destination.receive(amount);
};
destination.receive = function(amount){
destination.deposit(amount);
destination.log("deposited " + amount);
};
this.transfer = function(amount){
source.transfer(amount);
};
}
Эта статья о моделях в AngularJS может помочь:
http://joelhooks.com/blog/2013/04/24/modeling-data-and-state-in-your-angularjs-application/
Более старый вопрос, но я думаю, что тема более актуальна, чем когда-либо, учитывая новое направление Angular 2.0. Я бы сказал, что наилучшей практикой является написание кода с как можно меньшим количеством зависимостей от конкретной среды. Используйте только те части каркаса, которые добавляют прямую ценность.
В настоящее время кажется, что сервис Angular - это одна из немногих концепций, которая перейдет к следующему поколению Angular, поэтому, вероятно, разумно следовать общему руководству по переносу всей логики на сервисы. Тем не менее, я бы сказал, что вы можете создавать разъединенные модели даже без прямой зависимости от сервисов Angular. Создание автономных объектов только с необходимыми зависимостями и обязанностями - это, вероятно, путь. Это также значительно облегчает жизнь при автоматизированном тестировании. В наши дни единственная ответственность - это шумная работа, но она имеет большой смысл!
Вот пример шаблона, который я считаю хорошим для отделения объектной модели от DOM.
http://www.syntaxsuccess.com/viewarticle/548ebac8ecdac75c8a09d58e
Основная цель состоит в том, чтобы структурировать ваш код так, чтобы его было так же легко использовать из модульных тестов, как и из представления. Если вы достигнете этого, вы сможете написать реалистичные и полезные тесты.
Как утверждают другие авторы, Angular не предоставляет готового базового класса для моделирования, но можно с пользой предоставить несколько функций:
- Методы взаимодействия с RESTful API и создания новых объектов
- Установление отношений между моделями
- Проверка данных перед сохранением в бэкэнде; также полезно для отображения ошибок в реальном времени
- Кэширование и отложенная загрузка, чтобы избежать расточительных HTTP-запросов
- Конечные автоматы (до / после сохранения, обновления, создания, нового и т. Д.)
Одна библиотека, которая делает все эти вещи хорошо, - это ngActiveResource ( https://github.com/FacultyCreative/ngActiveResource). Полное раскрытие - я написал эту библиотеку - и успешно использовал ее при создании нескольких приложений масштаба предприятия. Он хорошо протестирован и предоставляет API, который должен быть знаком разработчикам Rails.
Моя команда и я продолжаем активно развивать эту библиотеку, и мне бы очень хотелось, чтобы другие разработчики Angular внесли в нее свой вклад и протестировали ее.
Я попытался решить эту проблему в этом посте.
По сути, лучший дом для моделирования данных - это сервисы и фабрики. Однако, в зависимости от того, как вы извлекаете ваши данные, и от сложности поведения, которое вам нужно, есть много разных способов реализации. Angular в настоящее время не имеет стандартного способа или лучшей практики.
В публикации рассматриваются три подхода, использующих $ http, $ resource и Restangular.
Вот пример кода для каждого, с пользовательским getResult()
Метод на модели работы:
Restangle (легкий горох):
angular.module('job.models', [])
.service('Job', ['Restangular', function(Restangular) {
var Job = Restangular.service('jobs');
Restangular.extendModel('jobs', function(model) {
model.getResult = function() {
if (this.status == 'complete') {
if (this.passed === null) return "Finished";
else if (this.passed === true) return "Pass";
else if (this.passed === false) return "Fail";
}
else return "Running";
};
return model;
});
return Job;
}]);
Ресурс $ (немного более запутанный):
angular.module('job.models', [])
.factory('Job', ['$resource', function($resource) {
var Job = $resource('/api/jobs/:jobId', { full: 'true', jobId: '@id' }, {
query: {
method: 'GET',
isArray: false,
transformResponse: function(data, header) {
var wrapped = angular.fromJson(data);
angular.forEach(wrapped.items, function(item, idx) {
wrapped.items[idx] = new Job(item);
});
return wrapped;
}
}
});
Job.prototype.getResult = function() {
if (this.status == 'complete') {
if (this.passed === null) return "Finished";
else if (this.passed === true) return "Pass";
else if (this.passed === false) return "Fail";
}
else return "Running";
};
return Job;
}]);
$ http (хардкор):
angular.module('job.models', [])
.service('JobManager', ['$q', '$http', 'Job', function($q, $http, Job) {
return {
getAll: function(limit) {
var deferred = $q.defer();
$http.get('/api/jobs?limit=' + limit + '&full=true').success(function(data) {
var jobs = [];
for (var i = 0; i < data.objects.length; i ++) {
jobs.push(new Job(data.objects[i]));
}
deferred.resolve(jobs);
});
return deferred.promise;
}
};
}])
.factory('Job', function() {
function Job(data) {
for (attr in data) {
if (data.hasOwnProperty(attr))
this[attr] = data[attr];
}
}
Job.prototype.getResult = function() {
if (this.status == 'complete') {
if (this.passed === null) return "Finished";
else if (this.passed === true) return "Pass";
else if (this.passed === false) return "Fail";
}
else return "Running";
};
return Job;
});
В самом посте блога более подробно рассматриваются причины, по которым вы можете использовать каждый из этих подходов, а также примеры кода использования моделей в ваших контроллерах:
Модели данных AngularJS: $http VS $resource VS Ограниченный
Существует вероятность того, что Angular 2.0 предложит более надежное решение для моделирования данных, которое позволит всем на одной странице.