AngularJS: Compreendendo o padrão de design

147

Dentro do contexto de deste post de Igor Minar, líder do AngularJS:

MVC vs MVVM vs MVP . Que tópico controverso sobre o qual muitos desenvolvedores podem passar horas e horas debatendo e discutindo.

Por vários anos, o AngularJS esteve mais próximo do MVC (ou melhor, de uma de suas variantes do lado do cliente), mas com o tempo e graças a muitas refatorações e melhorias na API, agora está mais próximo do MVVM - o objeto $ scope pode ser considerado o ViewModel que está sendo decorado por uma função que chamamos de controlador .

Ser capaz de categorizar uma estrutura e colocá-la em um dos buckets MV * tem algumas vantagens. Ele pode ajudar os desenvolvedores a se sentirem mais confortáveis ​​com suas APIs, facilitando a criação de um modelo mental que representa o aplicativo que está sendo construído com a estrutura. Também pode ajudar a estabelecer a terminologia usada pelos desenvolvedores.

Dito isso, eu prefiro ver os desenvolvedores criar aplicativos chiques que são bem projetados e seguem a separação de preocupações, do que vê-los perder tempo discutindo sobre o absurdo do MV *. E por esse motivo, declaro que o AngularJS é a estrutura MVW - Model-View-Whatever . Onde o que quer que seja significa "o que funciona para você ".

O Angular oferece muita flexibilidade para separar bem a lógica da apresentação da lógica comercial e do estado da apresentação. Use-o para aumentar sua produtividade e capacidade de manutenção de aplicativos, em vez de discussões acaloradas sobre coisas que no final do dia não importam tanto.

Existem recomendações ou diretrizes para implementar o padrão de design do AngularJS MVW (Model-View-Whatever) em aplicativos do lado do cliente?

Artem Platonov
fonte
votado por ... do que vê-los perder tempo discutindo sobre MV * absurdo.
Shirgill Farhan
1
Você não precisa do Angular para seguir um padrão de design de classe de palavras.
usar o seguinte código

Respostas:

223

Graças a uma enorme quantidade de fontes valiosas, eu tenho algumas recomendações gerais para implementar componentes nos aplicativos AngularJS:


Controlador

  • O controlador deve ser apenas um interlayer entre o modelo e a visualização. Tente torná-lo o mais fino possível.

  • É altamente recomendável evitar a lógica de negócios no controlador. Deve ser movido para o modelo.

  • O controlador pode se comunicar com outros controladores usando a invocação de método (possível quando os filhos desejam se comunicar com os pais) ou os métodos $ emit , $ broadcast e $ on . As mensagens emitidas e transmitidas devem ser reduzidas ao mínimo.

  • O controlador não deve se preocupar com a apresentação ou manipulação do DOM.

  • Tente evitar controladores aninhados . Nesse caso, o controlador pai é interpretado como modelo. Injete modelos como serviços compartilhados.

  • O escopo no controlador deve ser usado para vincular o modelo com vista e
    encapsular o modelo de vista como para o padrão de design do modelo de apresentação .


Escopo

Trate o escopo como somente leitura em modelos e somente gravação em controladores . O objetivo do escopo é se referir ao modelo, não ao modelo.

Ao fazer a ligação bidirecional (modelo ng), certifique-se de não ligar diretamente às propriedades do escopo.


Modelo

O modelo no AngularJS é um singleton definido pelo serviço .

O modelo fornece uma excelente maneira de separar dados e exibição.

Os modelos são candidatos principais para testes de unidade, pois normalmente possuem exatamente uma dependência (alguma forma de emissor de evento, no caso comum o $ rootScope ) e contêm lógica de domínio altamente testável .

  • O modelo deve ser considerado como uma implementação de determinada unidade. É baseado no princípio de responsabilidade única. Unidade é uma instância que é responsável por seu próprio escopo de lógica relacionada, que pode representar uma entidade única no mundo real e descrevê-la no mundo da programação em termos de dados e estado .

  • O modelo deve encapsular os dados do aplicativo e fornecer uma API para acessar e manipular esses dados.

  • O modelo deve ser portátil para que possa ser facilmente transportado para aplicação semelhante.

  • Ao isolar a lógica da unidade em seu modelo, você facilitou a localização, atualização e manutenção.

  • O modelo pode usar métodos de modelos globais mais gerais, comuns a todo o aplicativo.

  • Tente evitar a composição de outros modelos em seu modelo usando injeção de dependência, se não for realmente dependente de diminuir o acoplamento de componentes e aumentar a testabilidade e a usabilidade da unidade .

  • Tente evitar o uso de listeners de eventos nos modelos. Isso os torna mais difíceis de testar e geralmente mata modelos em termos de princípio de responsabilidade única.

Implementação do modelo

Como o modelo deve encapsular alguma lógica em termos de dados e estado, ele deve restringir arquitetonicamente o acesso a seus membros, para que possamos garantir um acoplamento flexível.

A maneira de fazer isso no aplicativo AngularJS é defini-lo usando o tipo de serviço de fábrica . Isso nos permitirá definir propriedades e métodos privados com muita facilidade e também retornar os acessíveis publicamente em um único local, o que o tornará realmente legível para o desenvolvedor.

Um exemplo :

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

Criando novas instâncias

Tente evitar ter uma fábrica que retorne uma nova função capaz, pois isso começa a interromper a injeção de dependência e a biblioteca se comportará de maneira desajeitada, principalmente para terceiros.

Uma maneira melhor de realizar a mesma coisa é usar a fábrica como uma API para retornar uma coleção de objetos com métodos getter e setter anexados a eles.

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
    // ...
  };
});

Modelo Global

Em geral, tente evitar tais situações e projetar seus modelos adequadamente, para que possam ser injetados no controlador e usados ​​em sua visão.

Em particular, alguns métodos requerem acessibilidade global dentro do aplicativo. Para tornar isso possível, você pode definir a propriedade ' common ' em $ rootScope e vinculá-la ao commonModel durante a inicialização do aplicativo:

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

Todos os seus métodos globais viverão dentro de propriedades ' comuns '. Este é algum tipo de espaço para nome .

Mas não defina nenhum método diretamente no seu $ rootScope . Isso pode levar a um comportamento inesperado quando usado com a diretiva ngModel dentro do seu escopo de exibição, geralmente desarrumando seu escopo e levando a métodos de escopo a substituir problemas.


Recurso

O recurso permite que você interaja com diferentes fontes de dados .

Deve ser implementado usando o princípio de responsabilidade única .

Em particular, é um proxy reutilizável para terminais HTTP / JSON.

Os recursos são injetados nos modelos e oferecem a possibilidade de enviar / recuperar dados.

Implementação de recursos

Uma fábrica que cria um objeto de recurso que permite interagir com fontes de dados RESTful do lado do servidor.

O objeto de recurso retornado possui métodos de ação que fornecem comportamentos de alto nível sem a necessidade de interagir com o serviço $ http de baixo nível.


Serviços

Modelo e recurso são serviços .

Os serviços não são associados, são fracamente acoplados unidades de funcionalidade que são independentes.

Os serviços são um recurso que o Angular traz para os aplicativos Web do lado do cliente, do lado do servidor, onde os serviços costumam ser usados ​​há muito tempo.

Serviços em aplicativos angulares são objetos substituíveis que são conectados usando injeção de dependência.

Angular vem com diferentes tipos de serviços. Cada um com seus próprios casos de uso. Leia Noções básicas sobre tipos de serviço para obter detalhes.

Tente considerar os principais princípios da arquitetura de serviço em seu aplicativo.

Em geral, de acordo com o Glossário de Serviços da Web :

Um serviço é um recurso abstrato que representa a capacidade de executar tarefas que formam uma funcionalidade coerente do ponto de vista das entidades provedoras e solicitantes. Para ser usado, um serviço deve ser realizado por um agente fornecedor concreto.


Estrutura do lado do cliente

Em geral, o lado do cliente do aplicativo é dividido em módulos . Cada módulo deve ser testável como uma unidade.

Tente definir módulos dependendo do recurso / funcionalidade ou exibição , não por tipo. Veja a apresentação de Misko para detalhes.

Os componentes do módulo podem ser agrupados convencionalmente por tipos como controladores, modelos, visualizações, filtros, diretivas etc.

Mas o próprio módulo permanece reutilizável , transferível e testável .

Também é muito mais fácil para os desenvolvedores encontrarem algumas partes do código e todas as suas dependências.

Por favor, consulte Organização de código em aplicativos AngularJS e JavaScript grandes para obter detalhes.

Um exemplo de estruturação de pastas :

|-- 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

Um bom exemplo de estruturação angular de aplicativos é implementado pelo angular-app - https://github.com/angular-app/angular-app/tree/master/client/src

Isso também é considerado pelos geradores de aplicativos modernos - https://github.com/yeoman/generator-angular/issues/109

Artem Platonov
fonte
5
Eu tenho uma preocupação com: "É altamente recomendável evitar a lógica de negócios no controlador. Ele deve ser movido para o modelo". No entanto, a partir da documentação oficial, você pode ler: "Em geral, um Controlador não deve tentar fazer muito. Ele deve conter apenas a lógica de negócios necessária para uma única exibição". Estamos falando sobre a mesma coisa?
op1ekun
3
Eu diria - trate o Controller como View Model.
Artem Platonov
1
+1. Alguns ótimos conselhos aqui! 2. Infelizmente, o exemplo de searchModelnão segue os conselhos de reutilização. Seria melhor a importação das constantes através do constantserviço. 3. Qualquer explicação que se entende aqui ?:Try to avoid having a factory that returns a new able function
Dmitri Zaitsev
1
Também a substituição do objeto prototypequebras de propriedade da herança, em vez pode-se usarCar.prototype.save = ...
Dmitri Zaitsev
2
@ChristianAichinger, trata-se da natureza da cadeia de protótipos JavaScript que obriga a usar uma objectexpressão de ligação bidirecional para garantir que você escreva na propriedade ou setterfunção exata . No caso de usar a propriedade direta do seu escopo ( sem um ponto ), você corre o risco de ocultar a propriedade de destino desejada com a recém-criada no escopo superior mais próximo da cadeia de protótipos ao escrever nela. Isto é melhor explicado na apresentação do Misko
Artem Platonov
46

Acredito que a opinião de Igor sobre isso, como pode ser visto na citação que você forneceu, seja apenas a ponta do iceberg de um problema muito maior.

MVC e seus derivados (MVP, PM, MVVM) são bons e elegantes dentro de um único agente, mas uma arquitetura servidor-cliente é para todos os efeitos um sistema de dois agentes, e as pessoas geralmente são tão obcecadas com esses padrões que esquecem que o problema em questão é muito mais complexo. Ao tentar aderir a esses princípios, eles acabam tendo uma arquitetura defeituosa.

Vamos fazer isso pouco a pouco.

As diretrizes

Visualizações

Dentro do contexto Angular, a visualização é o DOM. As diretrizes são:

Faz:

  • Variável de escopo atual (somente leitura).
  • Ligue para o controlador para ações.

Não faça:

  • Coloque qualquer lógica.

Tão tentador, curto e inofensivo:

ng-click="collapsed = !collapsed"

Significa praticamente qualquer desenvolvedor que agora compreenda como o sistema funciona para inspecionar os arquivos Javascript e HTML.

Controladores

Faz:

  • Vincule a vista ao 'modelo' colocando dados no escopo.
  • Responda às ações do usuário.
  • Lidar com a lógica de apresentação.

Não faça:

  • Lide com qualquer lógica de negócios.

A razão para a última diretriz é que controladores são irmãs de visões, não entidades; nem são reutilizáveis.

Você poderia argumentar que as diretivas são reutilizáveis, mas as diretivas também são irmãs de visualizações (DOM) - elas nunca foram destinadas a corresponder a entidades.

Claro, algumas vezes as visualizações representam entidades, mas esse é um caso bastante específico.

Em outras palavras, os controladores devem se concentrar na apresentação - se você aplicar a lógica de negócios, não apenas terá um controlador inflável e pouco gerenciável, mas também violará o princípio da separação de interesses .

Como tal, os controladores no Angular são realmente mais do Presentation Model ou MVVM .

E assim, se os controladores não deveriam lidar com a lógica de negócios, quem deveria?

O que é um modelo?

Seu modelo de cliente geralmente é parcial e obsoleto

A menos que você esteja escrevendo um aplicativo Web offline ou um aplicativo extremamente simples (poucas entidades), é altamente provável que seu modelo de cliente seja:

  • Parcial
    • Ou ele não tem todas as entidades (como no caso de paginação)
    • Ou não possui todos os dados (como no caso da paginação)
  • Antigo - Se o sistema tiver mais de um usuário, a qualquer momento você não poderá ter certeza de que o modelo que o cliente mantém é o mesmo que o servidor.

O modelo real deve persistir

No MCV tradicional, o modelo é a única coisa que persiste . Sempre que falamos de modelos, eles devem ser persistidos em algum momento. Seu cliente pode manipular modelos à vontade, mas até que a ida e volta ao servidor seja concluída com êxito, o trabalho não será concluído.

Consequências

Os dois pontos acima devem servir como cautela - o modelo que seu cliente possui pode envolver apenas uma lógica comercial parcial, na maioria simples.

Como tal, talvez seja sábio, dentro do contexto do cliente, usar letras minúsculas M- portanto, é realmente mVC , mVP e mVVm . O grande Mé para o servidor.

Logíca de negócios

Talvez um dos conceitos mais importantes sobre modelos de negócios seja que você pode subdividi-los em dois tipos (eu omito a terceira visão de negócios, pois essa é uma história para outro dia):

  • Lógica de domínio - também conhecida como regras de negócios corporativos , a lógica que é independente de aplicativo. Por exemplo, forneça um modelo com firstNamee sirNamepropriedades, um getter como getFullName()pode ser considerado independente de aplicativo.
  • Lógica do aplicativo - também conhecida como regras de negócios do aplicativo , que é específica do aplicativo. Por exemplo, verificações e manipulação de erros.

É importante enfatizar que ambos no contexto do cliente não são lógicas de negócios 'reais' - eles lidam apenas com a parte importante para o cliente. A lógica do aplicativo (não a lógica do domínio) deve ter a responsabilidade de facilitar a comunicação com o servidor e a maior parte da interação do usuário; enquanto a lógica do domínio é amplamente em pequena escala, específica da entidade e orientada à apresentação.

A questão ainda permanece - onde você as joga dentro de uma aplicação angular?

Arquitetura de 3 vs 4 camadas

Todas essas estruturas MVW usam 3 camadas:

Três círculos  Interior - modelo, meio - controlador, externo - vista

Mas há duas questões fundamentais com isso quando se trata de clientes:

  • O modelo é parcial, obsoleto e não persiste.
  • Não há lugar para colocar a lógica do aplicativo.

Uma alternativa para essa estratégia é a estratégia de 4 camadas :

4 círculos, do interno para o externo - Regras comerciais da empresa, Regras comerciais da aplicação, Adaptadores de interface, Estruturas e drivers

O negócio real aqui é a camada de regras de negócios do aplicativo (Casos de Uso), que geralmente fica errada nos clientes.

Essa camada é realizada pelos interatores (tio Bob), que é praticamente o que Martin Fowler chama de camada de serviço de script de operação .

Exemplo concreto

Considere o seguinte aplicativo da web:

  • O aplicativo mostra uma lista paginada de usuários.
  • O usuário clica em 'Adicionar usuário'.
  • Um modelo é aberto com um formulário para preencher os detalhes do usuário.
  • O usuário preenche o formulário e pressiona enviar.

Algumas coisas devem acontecer agora:

  • O formulário deve ser validado pelo cliente.
  • Uma solicitação deve ser enviada ao servidor.
  • Um erro deve ser tratado, se houver um.
  • A lista de usuários pode ou não (devido à paginação) precisar ser atualizada.

Onde jogamos tudo isso?

Se sua arquitetura envolve um controlador que chama $resource, tudo isso acontecerá dentro do controlador. Mas existe uma estratégia melhor.

Uma solução proposta

O diagrama a seguir mostra como o problema acima pode ser resolvido adicionando outra camada lógica do aplicativo nos clientes Angular:

4 caixas - DOM aponta para Controller, que aponta para Application logic, que aponta para $ resource

Então, adicionamos uma camada entre o controlador ao $ resource, essa camada (vamos chamá-lo de interator ):

  • É um serviço . No caso de usuários, pode ser chamado UserInteractor.
  • Ele fornece métodos correspondentes a casos de uso , encapsulando a lógica do aplicativo .
  • Ele controla as solicitações feitas ao servidor. Em vez de um controlador que chama $ resource com parâmetros de formato livre, essa camada garante que as solicitações feitas ao servidor retornem dados nos quais a lógica do domínio possa atuar.
  • Decora a estrutura de dados retornada com o protótipo de lógica de domínio .

E assim, com os requisitos do exemplo concreto acima:

  • O usuário clica em 'Adicionar usuário'.
  • O controlador solicita ao interator um modelo de usuário em branco, decorado com o método de lógica de negócios, como validate()
  • Após o envio, o controlador chama o validate()método de modelo .
  • Se falhar, o controlador lida com o erro.
  • Se for bem-sucedido, o controlador chama o interator com createUser()
  • O interator chama $ resource
  • Após a resposta, o interator delega qualquer erro ao controlador, que lida com eles.
  • Após uma resposta bem-sucedida, o interator garante que, se necessário, a lista de usuários seja atualizada.
Izhaki
fonte
Portanto, AngularJS é definido MVW (onde W é o que for), pois eu posso optar por ter um Controller (com toda a lógica de negócios) ou um View Model / Presenter (sem lógica de negócios, mas apenas algum código para preencher a visualização) com BL um serviço separado? Estou certo?
BAD_SEED
Melhor resposta. Você tem um exemplo real no GitHub de um aplicativo angular de quatro camadas?
RPallas
1
@Rallas, não, não (gostaria de ter tempo para isso). No momento, estamos testando uma arquitetura em que a 'lógica da aplicação' é apenas um interator de fronteira; um resolvedor entre ele e o controlador e um modelo de vista que tenha alguma lógica de vista. Ainda estamos experimentando, portanto não 100% dos prós ou contras. Mas uma vez feito, espero escrever um blog em algum lugar.
Izhaki 18/09/2015
1
@heringer Basicamente, introduzimos modelos - construções OOP que representam entidades de domínio. São esses modelos que se comunicam com recursos, não com controladores. Eles encapsulam a lógica do domínio. Os controladores chamam modelos, que por sua vez chamam recursos.
Izhaki 27/07/16
1
@ alex440 Não. Embora já se passaram dois meses que um post sério sobre esse tópico está na ponta dos meus dedos. O Natal está chegando - possivelmente então.
Izhaki 02/12/16
5

Um pequeno problema em comparação aos grandes conselhos da resposta de Artem, mas em termos de legibilidade do código, achei melhor definir a API completamente dentro do returnobjeto, para minimizar a alternância entre o código e a aparência das variáveis ​​definidas:

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, ...) {
    ...
    }
  }
});

Se o returnobjeto parecer "muito cheio", isso é um sinal de que o Serviço está fazendo muito.

Dmitri Zaitsev
fonte
0

O AngularJS não implementa o MVC da maneira tradicional, mas implementa algo mais próximo do MVVM (Model-View-ViewModel), o ViewModel também pode ser chamado de fichário (no caso angular, pode ser $ scope). O Modelo -> Como sabemos, o modelo em angular pode ser apenas objetos JS antigos simples ou os dados em nosso aplicativo

A View -> a view em angularJS é o HTML que foi analisado e compilado por angularJS aplicando as diretivas, instruções ou ligações. O ponto principal aqui é que a entrada não é apenas a string HTML simples (innerHTML), mas sim é o DOM criado pelo navegador.

O ViewModel -> ViewModel é na verdade o fichário / ponte entre sua visualização e modelo no caso do angularJS, é $ scope, para inicializar e aumentar o $ scope que usamos Controller.

Se eu quiser resumir a resposta: No aplicativo angularJS, $ scope tem referência aos dados, o Controller controla o comportamento e o View lida com o layout, interagindo com o controller para se comportar de acordo.

Ashutosh
fonte
-1

Para ser mais preciso, a Angular usa diferentes padrões de design que já encontramos em nossa programação regular. 1) Quando registramos nossos controladores ou diretivas, fábrica, serviços etc. em relação ao nosso módulo. Aqui está ocultando os dados do espaço global. Qual é o padrão do módulo . 2) Quando o angular usa sua verificação suja para comparar as variáveis ​​de escopo, aqui ele usa o Padrão do Observador . 3) Todos os escopos filho pai em nossos controladores usam padrão Prototypal. 4) No caso de injetar os serviços, utiliza o Padrão de Fábrica .

No geral, ele usa diferentes padrões de design conhecidos para resolver os problemas.

Naveen Reddy
fonte