Onde colocar dados e comportamento do modelo? [tl; dr; Use Services]

341

Estou trabalhando com o AngularJS para o meu projeto mais recente. Na documentação e nos tutoriais, todos os dados do modelo são colocados no escopo do controlador. Eu entendo que é preciso estar lá para estar disponível para o controlador e, portanto, dentro das visualizações correspondentes.

No entanto, acho que o modelo não deve ser implementado lá. Pode ser complexo e ter atributos particulares, por exemplo. Além disso, pode-se querer reutilizá-lo em outro contexto / aplicativo. Colocar tudo no controlador quebra totalmente o padrão MVC.

O mesmo vale para o comportamento de qualquer modelo. Se eu usasse a arquitetura DCI e separasse o comportamento do modelo de dados, teria que introduzir objetos adicionais para manter o comportamento. Isso seria feito através da introdução de papéis e contextos.

DCI == D ATA C OLABORAÇÃO eu nteração

Obviamente, os dados e o comportamento do modelo podem ser implementados com objetos javascript simples ou qualquer padrão de "classe". Mas qual seria a maneira do AngularJS fazer isso? Usando serviços?

Então, tudo se resume a esta pergunta:

Como você implementa modelos dissociados do controlador, seguindo as práticas recomendadas do AngularJS?

Nils Blum-Oeste
fonte
12
Eu votaria nesta questão se você pudesse definir o DCI ou pelo menos fornecer o formulário explicitado. Eu nunca vi esse acrônimo em nenhuma literatura de software. Obrigado.
Jim Raden
13
Acabei de adicionar um link para o DCI como referência.
Nils Blum-Oeste
11
@JimRaden DCI é Dataq, Contexto, interação e é um paradigma formulado primeiramente pelo pai da MVC (Trygve Reenskauge). Já há bastante literatura sobre o assunto. Uma boa leitura é Coplien e Bjørnvig "arquitetura enxuta"
Rune FS
3
Obrigado. Para o bem ou para o mal, a maioria das pessoas nem conhece a literatura original até agora. Existem 55 milhões de artigos sobre MVC, segundo o Google, mas apenas 250.000 mencionam MCI e MVC. E no Microsoft.com? 7. O AngularJS.org nem sequer menciona o acrônimo do DCI: "Sua pesquisa - site: angularjs.org dci - não encontrou nenhum documento".
Jim Raden
Objetos de recursos são basicamente os modelos do Angular.js. Os estão estendendo.
Salman von Abbas

Respostas:

155

Você deve usar serviços se quiser algo utilizável por vários controladores. Aqui está um exemplo simples e artificial:

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
}
Andrew Joslin
fonte
23
Qual seria o benefício de usar um serviço, apenas criando um objeto Javascript simples como modelo e atribuindo-o ao escopo do controlador?
Nils Blum-Oeste
22
No caso de você precisar da mesma lógica compartilhada entre vários controladores. Além disso, é mais fácil testar as coisas de maneira independente.
Andrew Joslin
11
O último exemplo meio que sugado, este faz mais sentido. Eu editei.
Andrew Joslin
9
Sim, com um objeto Javascript antigo simples, você não seria capaz de injetar nada Angular no seu ListService. Como neste exemplo, se você precisasse de $ http.get para recuperar os dados da Lista no início ou se precisasse injetar $ rootScope para poder transmitir $ eventos.
Andrew Joslin
11
Para tornar este exemplo mais parecido com o DCI, os dados não devem estar fora do ListService?
PiTheNumber
81

Atualmente, estou tentando esse padrão, que, embora não seja o DCI, fornece um desacoplamento clássico de serviço / modelo (com serviços para conversar com serviços da web (também conhecido como modelo CRUD) e modelo que define as propriedades e métodos do objeto).

Observe que eu só uso esse padrão sempre que o objeto de modelo precisar de métodos trabalhando em suas próprias propriedades, que provavelmente usarei em qualquer lugar (como getter / setters aprimorados). Estou não defendendo fazendo isso por todos os serviços de forma sistemática.

Edição: Eu costumava pensar que esse padrão iria contra o mantra "modelo angular é simples objeto javascript antigo", mas parece-me agora que esse padrão está perfeitamente bem.

EDIT (2): Para ser mais claro, eu uso uma classe Model apenas para fatorar getters / setters simples (por exemplo: para serem usados ​​em modelos de exibição). Para a lógica de grandes negócios, recomendo o uso de serviços separados que "conhecem" o modelo, mas são mantidos separados deles e incluem apenas a lógica de negócios. Chame de camada de serviço "especialista em negócios", se desejar

service / ElementServices.js (observe como o Element é injetado na declaração)

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 (usando o angularjs Factory, criado para criação de objeto)

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;
});
Ben G
fonte
4
Estou entrando no Angular, mas ficaria curioso para saber se / por que os veteranos pensariam que isso é heresia. Esta é provavelmente a maneira que eu abordaria inicialmente também. Alguém poderia fornecer algum feedback?
Aaronius
2
@ Aaronius só para esclarecer: eu nunca li "você nunca deve fazer isso" em qualquer documento ou blog angularjs, mas eu sempre li coisas como "angularjs não precisa de um modelo, é apenas usando javascript antigo" e tive que descobrir esse padrão por conta própria. Como este é o meu primeiro projeto real no AngularJS, estou colocando esses avisos fortes, para que as pessoas não copiem / colem sem pensar primeiro.
Ben G
Eu estabeleci um padrão aproximadamente semelhante. É uma pena que Angular não tenha nenhum apoio real (ou aparentemente deseje apoiar) um modelo no sentido "clássico".
drt
3
Isso não me parece uma heresia, você está usando fábricas para o que elas foram criadas: construir objetos. Acredito que a frase "angularjs não precisa de um modelo" significa "você não precisa herdar de uma classe especial ou usar métodos especiais (como ko.observable, nocaute) para trabalhar com modelos em angular, um o objeto js puro será suficiente ".
Felipe Castro
11
Ter um ElementService chamado apropriadamente para cada coleção resultaria em um monte de arquivos quase idênticos?
Collin Allen
29

A documentação do Angularjs afirma claramente:

Ao contrário de muitas outras estruturas, o Angular não faz restrições ou requisitos no modelo. Não há classes para herdar ou métodos acessadores especiais para acessar ou alterar o modelo. O modelo pode ser primitivo, hash do objeto ou um tipo de objeto completo. Em resumo, o modelo é um objeto JavaScript simples.

- Guia do desenvolvedor do AngularJS - Conceitos da V1.5 - Modelo

Então, isso significa que você decide como declarar um modelo. É um simples objeto Javascript.

Pessoalmente, não utilizarei os Serviços Angular, pois eles deveriam se comportar como objetos singleton que você pode usar, por exemplo, para manter estados globais em seu aplicativo.

SC
fonte
Você deve fornecer um link para o local indicado na documentação. Eu fiz uma pesquisa no Google por "Angular não faz restrições ou requisitos para o modelo" , e ele não aparece em nenhum lugar nos documentos oficiais, até onde eu sei.
4
estava nos antigos documentos angularjs (o que estava vivo enquanto respondia): github.com/gitsome/docular/blob/master/lib/angular/ngdocs/guide/…
SC
8

O DCI é um paradigma e, como tal, não existe uma maneira angular do JS de fazê-lo, o idioma suporta o DCI ou não. JS suporta DCI bastante bem se você estiver disposto a usar a transformação de origem e, com algumas desvantagens, se não estiver. Novamente, o DCI não tem mais a ver com injeção de dependência do que dizer que uma classe C # tem e definitivamente também não é um serviço. Portanto, a melhor maneira de fazer o DCI com o angulusJS é fazê-lo da maneira JS, o que é bem próximo de como o DCI é formulado em primeiro lugar. A menos que você faça a transformação de origem, você não poderá executá-lo totalmente, pois os métodos de função farão parte do objeto mesmo fora do contexto, mas esse geralmente é o problema com o DCI baseado em injeção de método. Se você olhar para fullOO.infoNo site oficial do DCI, você pode dar uma olhada nas implementações do ruby, elas também usam injeção de método ou você pode dar uma olhada aqui para obter mais informações sobre o DCI. É principalmente com exemplos de RUby, mas o material DCI é independente disso. Uma das chaves do DCI é que o que o sistema faz é separado do que é o sistema. Portanto, o objeto de dados é bastante tolo, mas uma vez vinculado a uma função em um contexto, os métodos de função disponibilizam determinado comportamento. Uma função é simplesmente um identificador, nada mais, e ao acessar um objeto através desse identificador, os métodos de função estão disponíveis. Não há nenhum objeto / classe de função. Com a injeção de método, o escopo dos métodos de função não é exatamente como descrito, mas aproximado. Um exemplo de contexto em JS pode ser

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);
   };
}
Rune FS
fonte
11
Obrigado por elaborar o material da DCI. É uma ótima leitura. Mas minhas perguntas realmente apontam para "onde colocar os objetos de modelo em angularjs". O DCI está lá apenas para referência, que eu posso não apenas ter um modelo, mas dividi-lo da maneira DCI. Editará a pergunta para torná-la mais clara.
Nils Blum-Oeste
7

Este artigo sobre modelos no AngularJS pode ajudar:

http://joelhooks.com/blog/2013/04/24/modeling-data-and-state-in-your-angularjs-application/

marianboda
fonte
7
Observe que as respostas somente de link são desencorajadas; as respostas SO devem ser o ponto final de uma pesquisa por uma solução (em comparação com outra parada de referências, que tendem a ficar obsoletas ao longo do tempo). Considere adicionar aqui uma sinopse independente, mantendo o link como referência.
22413 kleopatra
adicionar esse link em um comentário sobre a pergunta seria bom.
jorrebor
Este link é realmente um artigo muito bom, mas ditto que precisaria ser trabalhada em uma resposta para ser adequado para SO
Jeremy Zerr
5

Conforme declarado em outros pôsteres, o Angular não fornece classe básica pronta para modelagem, mas é possível fornecer várias funções de maneira útil:

  1. Métodos para interagir com uma API RESTful e criar novos objetos
  2. Estabelecendo relacionamentos entre modelos
  3. Validando dados antes de persistir no back-end; também é útil para exibir erros em tempo real
  4. Armazenamento em cache e carregamento lento para evitar solicitações HTTP desnecessárias
  5. Ganchos de máquinas de estado (antes / depois de salvar, atualizar, criar, novo, etc)

Uma biblioteca que faz todas essas coisas bem é o ngActiveResource ( https://github.com/FacultyCreative/ngActiveResource ). Divulgação completa - escrevi esta biblioteca - e usei-a com sucesso na criação de vários aplicativos em escala corporativa. É bem testado e fornece uma API que deve ser familiar para os desenvolvedores do Rails.

Minha equipe e eu continuamos desenvolvendo ativamente essa biblioteca e adoraria ver mais desenvolvedores Angular contribuindo e testando a batalha.

Brett Cassette
fonte
Ei! Isso é realmente ótimo! Vou conectá-lo ao meu aplicativo agora. O teste de batalha acabou de começar.
193 J. Bruni
11
Eu estava apenas olhando para a sua postagem e fiquei imaginando quais seriam as diferenças entre o seu ngActiveResourcee o $resourceserviço da Angular . Sou um pouco novo no Angular e naveguei rapidamente nos dois conjuntos de documentos, mas eles parecem oferecer muita sobreposição. Foi ngActiveResourcedesenvolvido antes da $resourcedisponibilidade do serviço?
Eric B.
5

Uma pergunta mais antiga, mas acho que o tópico é mais relevante do que nunca, dada a nova direção do Angular 2.0. Eu diria que uma prática recomendada é escrever código com o mínimo de dependências possível em uma estrutura específica. Use apenas as partes específicas da estrutura em que agrega valor direto.

Atualmente, parece que o serviço Angular é um dos poucos conceitos que chegarão à próxima geração do Angular, por isso é provavelmente inteligente seguir a diretriz geral de mover toda a lógica para os serviços. No entanto, eu argumentaria que você pode criar modelos dissociados, mesmo sem uma dependência direta dos serviços Angular. Criar objetos independentes com apenas dependências e responsabilidades necessárias é provavelmente o caminho a percorrer. Também facilita muito a vida ao realizar testes automatizados. Nos dias de hoje, a responsabilidade única é um trabalho movimentado, mas faz muito sentido!

Aqui está um exemplo de um padrão que considero bom para dissociar o modelo de objeto do dom.

http://www.syntaxsuccess.com/viewarticle/548ebac8ecdac75c8a09d58e

Um dos principais objetivos é estruturar seu código de maneira a torná-lo tão fácil de usar em testes de unidade quanto em uma exibição. Se você conseguir isso, estará bem posicionado para escrever testes realistas e úteis.

TGH
fonte
4

Eu tentei resolver esse problema exato nesta postagem do blog .

Basicamente, o melhor local para modelagem de dados é em serviços e fábricas. No entanto, dependendo de como você recupera seus dados e da complexidade dos comportamentos necessários, existem várias maneiras diferentes de executar a implementação. Angular atualmente não possui padrão maneira ou uma melhor prática.

A publicação aborda três abordagens, usando $ http , $ resource e Restangular .

Aqui está um exemplo de código para cada um, com um getResult()método personalizado no modelo de trabalho:

Restangular (fácil camurça):

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;
  }]);

$ resource (um pouco mais complicado):

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 (incondicional):

angular.module('job.models', [])
    .service('JobManager', ['$http', 'Job', function($http, Job) {
        return {
            getAll: function(limit) {
                var params = {"limit": limit, "full": 'true'};
                return $http.get('/api/jobs', {params: params})
                  .then(function(response) {
                    var data = response.data;
                    var jobs = [];
                    for (var i = 0; i < data.objects.length; i ++) {
                        jobs.push(new Job(data.objects[i]));
                    }
                    return jobs;
                });
            }
        };
    }])
    .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;
    });

A postagem do blog detalha o motivo por que você pode usar cada abordagem, bem como exemplos de código de como usar os modelos em seus controladores:

Modelos de dados AngularJS: $ http VS $ resource VS Restangular

Existe a possibilidade do Angular 2.0 oferecer uma solução mais robusta para modelagem de dados que coloca todos na mesma página.

Alan Christopher Thomas
fonte