Injetando serviços em componentes de forma transparente

Este é meu primeiro artigo aqui no blog. É uma honra estar aqui com vocês e o meu objetivo é mostrar como usar serviços nos seus componentes de uma forma madura e organizada.

Você provavelmente já viu alguns exemplos de aplicações utilizando Vue. Já deve ter percebido que ainda há uma fase de fragmentação quanto ao uso da ferramenta, onde vários grupos tem várias arquiteturas sendo aplicadas e testadas.

Esta situação é um pouco desconfortável para alguns, mas ela espelha um momento cada vez mais comum no desenvolvimento web: POSSIBILIDADES.

É muito provável que se você perguntar a cada arquiteto de cada estrutura visitada ele tenha partido de alguma experiência anterior pela qual passou e tenha organizado sua estrutura porque a que vinha usando o deixou na mão ou não escalou nesta ou naquela situação. Conosco não é muito diferente. Nosso caso era realmente a preocupação com a flexibilidade e rapidez. Precisávamos de estruturas que fossem criadas rapidamente e que fossem simples de serem atualizadas, sem ter que abrir centenas de arquivos para aplicar mudanças; esse era nosso desafio.

Disclaimer: o conteúdo do artigo são apenas sugestões de coisas que usamos no nosso dia-a-dia, fique à vontade para concordar e/ou discordar, aproveitando as experiências da forma que melhor lhe convir.

Serviços

Nos contextos de arquitetura de software o termo serviço refere-se a uma funcionalidade ou um conjunto de funcionalidades de software com um propósito que pode ser reutilizado em diferentes ocasiões. Geralmente associamos os acessos à recursos HTTP ao nome Service, mas nem sempre usamos uma estrutura dedicada à isso para cada domínio da nossa aplicação, o que, particularmente se tornou um problema.

Devemos ter cuidado ao usar recursos como this.$http.get('/api/v1/products') dentro da nossa aplicação, pois estamos pulverizando uma camada lógica importante por toda a aplicação.

Vou deixar o artigo “Vue.js: Trabalhando com Serviços” como referência para construções de serviços, caso queira se aprofundar um pouco mais sobre o tema.

Mantendo Contratos

Para quem está acostumado com linguagens onde a Orientação a Objeto possui suporte à recursos como interfaces, métodos abstratos, enum, entre outros, o javascript se torna um pouco assustador por não entregar esse tipo de composição. Contudo, isso não é exatamente um problema quando você organiza a sua aplicação porque essa chamada falta de rigidez pode ser compensada de outras formas para forçar que alguns caminhos não sejam seguidos. Sugiro dar uma olhada no artigo “Vue.js ajax patterns” para pegar algumas dicas de composição de serviços.

Criando Serviços Extensíveis

Para poder seguir com este material apresentaremos uma solução simples para criar um Service que pode ser estendido e usado nos domínios das entidades com as quais estão lidando. A seguir será apresentada uma lista de documentos e a respectiva explicação sobre o que representa cada um deles.

A estrutura

/src
 |
 ├── /domains/
 |   └── /Product
 |        └── /Service
 |             └── Product.js
 |
 ├── /service
 |   ├── helper.js
 |   ├── Api.js
 |   └── Service.js
 |
 └── main.js
src/main.js

Um pequeno exemplo de uso de como o serviço será usado. Ele importa o service de Product e constrói uma instância para poder executar o método read e imprimir a saída;

import Product from 'src/domains/Product/Service/Product'

const product = Product.build({})

product.read().then(console.warn)
src/domains/Product/Service/Product.js

A especialização do serviço Api para ser usado no escopo da entidade Product;

import Api from 'src/service/Api'

export class Product extends Api {
  /**
   * @param {*} options
   */
  constructor(options) {
    super(options, '/api/v1/market/products')
  }
}
src/service/Api.js

Serviço para consumo de API’s que entrega 4 métodos básicos para realizar o que é costumeiramente chamado de CRUD;

import Service from 'src/service/Service'
import { promise } from 'src/service/helper'

/**
 * @type {Api}
 */
export default class Api extends Service {
  /**
   * @param {Object} options
   * @param {string} resource
   */
  constructor (options, resource) {
    super(options)
    this.resource = resource
  }

  /**
   * @param {Object} record
   */
  create (record) {
    return promise({status: 'CREATE'})
  }

  /**
   * @param {string} id
   */
  read (id) {
    return promise([{status: 'READ'}])
  }

  /**
   * @param {Object} record
   */
  update (record) {
    return promise({status: 'UPDATE'})
  }

  /**
   * @param {Object} record
   */
  destroy (record) {
    return promise({status: 'DESTROY'})
  }
}
src/service/Service.js

Contrato básico para a factory de instâncias do Service;

/**
 * @type {Service}
 */
export default class Service {
  /**
   * @param {*} options
   */
  constructor (options) {
    this.options = options
  }

  /**
   * @param {*} options
   */
  static build (options) {
    return new this(options)
  }
}
src/service/helper.js

Apenas uma função fake para simular o delay da troca de contexto de da manipulação dos dados.

/**
 * @type {Function}
 */
export const promise = response => {
  return new Promise(function (resolve) {
    window.setTimeout(() => resolve(response), 1000)
  })
}

A saída exibida nesse caso é algo parecida com a imagem abaixo.

Com o que fora apresentado até aqui não é difícil perceber que podemos usar essa estrutura para representar diversas estratégias de API’s porque não está fechado o que o nosso serviço consome. Essa deve ser a visão do componente acerca do service: conhecer o contrato e não a implementação.

Embora o Javascript não tenha estruturas formais para contratos, é possível criar padrões para que a codificação componha esses contratos de forma orgânica.

Se quiser se aprofundar em abordagens de gestão de services pode dar uma olhada no artigo How not to suffer with APIs (dica que recebi do Vinicius Reis) onde é explorado em níveis bem profundos o uso do serviço como um modelo da entidade totalmente isolado da apresentação do componente.

Lidando com Componentes

Bom, se o serviço agora é uma estrutura totalmente segmentada do meu componente, como posso fazer uso desse recurso dentro do componente? A resposta dessa pergunta também é uma pergunta: quão flexível será seu componente e quão dinâmica será sua estrutura?

Importando serviços para o meu componente

Essa estratégia é bem simples e consiste basicamente em usar os módulos Javascript para usar o service dentro do escopo do seu componente.

<template>
  <table>
    <tr>
      <th>Name</th>
      <th>Price</th>
    </tr>
    <tr v-for="(row, key) in rows" :key="key">
      <td></td>
      <td></td>
    </tr>
  </table>
</template>

<script>
import { money } from 'src/support/formatter'
import Product from 'src/domains/Product/Service/Product'

const service = Product.build({})

export default {
  name: 'ProductTable',
  data: () => ({
    rows: []
  }),
  filters: {
    money: money
  },
  mounted () {
    service.read().then(rows => {
      this.rows = rows
    })
  }
}
</script>

Se precisar de uma estratégia mais eficiente de distribuir o service, pode usar um plugin para manipular o estado dele dentro do componente.

Seguindo essa abordagem teríamos o ProductTable.vue:

<template>
  ...
</template>

<script>
import { money } from 'src/support/formatter'
import Product from 'src/domain/product/service'

export default {
  name: 'ProductTable',
  service: Product.build({}),
  data: () => ({
    rows: []
  }),
  filters: {
    money: money
  },
  mounted () {
    this.$service.read().then(rows => {
      this.rows = rows
    })
  }
}
</script>

E teríamos uma modificação no protótipo do Vue através do src/service/Plugin.js:

export default Vue => {
  Object.defineProperty(Vue.prototype, '$service', {
    get () {
      if (this.$options.service) {
        return this.$options.service
      }
      if (this.$props.service) {
        return this.$props.service
      }
      throw new Error('The component doesn`t have a service')
    }
  });
}

Com esta segunda abordagem, mesmo que algum desenvolvedor sem querer faça:

this.$service = null

A resposta do Vue será contundente

Com a implementação do plugin feita dessa forma o service é passado para o componente e então ele “descobre” se este é um item das opções do construtor da instância do Vue ou se é uma prop do componente, entregando um elemento, que como vimos acima, é imutável; o que nos leva a próxima abordagem, passar os serviços via props.

Utilizando serviços como props

No ecossistema do Vue (na verdade é um conceito um pouco mais abrangente que isso) uma prop é um estado que é passado para um componente. Se não estiver recordando muito bem como funcionam essas propriedades dos componentes recomendo fazer uma visita à documentação antes de continuar.

Com a leitura em dia será fácil compreender que usando props tenho a capacidade de passar parâmetros para meus componentes sem ter que deliberadamente deixar esse parâmetro “hard-coded” dentro dele.

Para receber uma prop dentro de um componente é preciso declarar essa necessidade. O resultado é semelhante ao exemplo a seguir:

<template>
  ...
</template>

<script>
...

export default {
  name: 'ProductTable',
  props: {
    service: {
      required: true
    }
  },
  ...,
  mounted () {
    this.$service.read().then(rows => {
      this.rows = rows
    })
  }
}
</script>

O que implicaria em um caso de uso semelhante à:

<ProductTable v-bind:service="service"/>

Embora essa abordagem não seja ruim, ela possui alguns pontos que devem ser observados. Entre as situações que não me deixam confortável temos a questão de usar um estado do componente (uma propriedade no data) para abrigar o serviço que será mapeado à prop, e a necessidade imperativa de ter que criar um arquivo .vue para cada rota implementada.

Usando o Router para injetar dependência

Durante a construção de app’s mais robustos geralmente usamos um router. O Vue Router é uma ferramenta que faz parte do ecossistema do Vue e nos permite criar rotas para nossos componentes. Ele tem suporte a informar as props que serão usadas para montar o componente e isto está descrito neste trecho da documentação.

No caso do nosso exemplo, vamos explorar o Function Mode onde as props serão geradas por uma função que recebe a rota carregada como parâmetro. Com o service sendo uma prop poderemos passá-lo por parâmetro através da rota onde seu componente será montado.

Como podemos ver no exemplo abaixo, será possível combinar as duas abordagens no seu projeto. Poderemos usar um mesmo componente recebendo o service por props ou através de $options, template, render ou route porque o acesso ao $service será transparente para o componente.

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from 'src/components/HelloWorld'
import Service from 'src/domains/Product/Service/Product'
import Component from 'src/domains/Product/Components/ProductTable'

Vue.use(Router)

export default new Router({
  routes: [
    ...,
    {
      path: '/dashboard/products',
      component: Component,
      props: route => ({
        service: Service.build({})
      })
    }
  ]
})

Conclusão

O Vue é muito flexível e deixa à cargo da sua capacidade de manipulação do Javascript a criação do ecossistema em que o componente está inserido. Tenha sempre cuidado para escolher abordagens que te permitam escalar a sua aplicação de forma mais objetiva possível pesquisando e estudando o máximo que puder antes de começar a escrever seus códigos. Ademais, você pode começar a fazer uso dessas metodologias para fazer seus requests sem que seu componente perceba que algo mudou.

Isolar o comportamento dos serviços é uma boa pedida em tempos onde estão surgindo opções interessantes para manipulação de dados, como o GraphQL.

Fique à vontade para enviar suas dúvidas, sugestões e/ou elogios. Para falar mais sobre VueJS fique ligado na comunidade e em todo o seu ecossistema : )

VueJS Comunidades

Ah, já ia esquecendo, segue o link do repositório https://github.com/wilcorrea/transparent-services, fica a dica para seguir os commits que será possível ver a evolução da mesma forma que a estrutura foi evoluindo nos artigos.