Padrões de Design (design Patterns) para APIs WEB
Os padrões de design são formalizados nas Melhores Práticas que um programador pode usar para resolver problemas comuns ao projetar um aplicativo ou sistema, seje ele baseado em Web ou não. Neste artigo, gostaria de olhar para uma combinação de design aliado a padrões arquitetônicos que acredito podem ajudar a criar APIs da web que são mais flexíveis, fáceis de manter e mais fáceis de entender em qualquer linguagem de programação, além de poderem ser integradas com qualquer outra linguagem existente.
Como o próprio nome já diz, são Padrões, ou seja, um conceito de grandeza que serve como modelo com autoridade para ser seguido, e amplamente adotado; ou gabarito, para ser usado como guia para a finalização. Na engenharia de software, um padrão de projeto é uma solução geral que pode ser repetida para um problema comum não é um produto acabado que pode ser transformado diretamente em código, é uma descrição de um modelo de como resolver um problema que pode ser usado em muitas situações diferentes, como uma fórmula para a melhor solução possível, fornecendo paradigmas de desenvolvimento testados e comprovados.
Os principais objetivos do uso destes padrões, é a eficiência em requer a consideração de problemas que podem não se tornar visíveis até mais tarde na implementação, ajudando a prevenir problemas sutis que podem causar problemas maiores e melhorar a legibilidade do código. Os padrões de projeto, permitem que os desenvolvedores se comuniquem usando nomes bem conhecidos e compreendidos para interações de software, e que podem ser aprimorados com o tempo, tornando-os mais robustos do que os projetos.
APIs são meros contratos, que definem como aplicativos, serviços e componentes se comunicam entre sí. Os padrões de design de API fornecem um conjunto compartilhado de melhores práticas, especificações e padrões que garantem que as APIs sejam confiáveis e simples para outros desenvolvedores usarem e que garantem que suas APIs sejam consistentes, escalonáveis e flexíveis, onde você aprimorará o design das APIs mais comuns, além de descobrir técnicas para casos extremos complicados.
A Camada de serviço
A camada de serviço é uma interface comum para a lógica do aplicativo que diferentes clientes, como uma interface da web, uma ferramenta de linha de comando ou um trabalho agendado, podem usar. De acordo com Gregor Hohpe no livro Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions arquitetura do aplicativo Patterns Of Enterprise, a camada de serviço, a camada de serviço, define o limite de um aplicativo com uma camada que estabelece um conjunto de operações disponíveis e coordena a resposta do aplicativo em cada uma das operações. Em sua forma básica, uma camada de serviço fornece um conjunto de métodos que qualquer outro cliente pode usar.
Os próprios métodos da camada de serviço então implementam a lógica do negócio no aplicativo e fazem chamadas para os bancos de dados ou modelos internos existentes, não precisa ser uma classe propriamente dita, mas pode ser um conjunto de funções que são expostas publicamente, no contexto das APIs da web, as duas formas oferecem várias vantagens, como vemos.
Separação de preocupações e capacidade de testes: em vez de implementar todas as funcionalidades diretamente, por exemplo, nas ações de um controlador ou manipulador de rota, a camada de serviço permite testar a lógica da aplicação separada de como ela está sendo acessada. Uma CLI ou GUI ou Estrutura Web que usa a camada de serviço precisa apenas de testes de integração, em vez de testes da integração e da lógica do aplicativo e do negócio ao mesmo tempo.
Auto documentação: Novos desenvolvedores podem simplesmente ver o que o aplicativo ou serviço fornece no contexto, indiferente da linguagem de programação implementada, simplesmente lendo quais classes ou funções que podem ser chamadas, e sem se preocupar de ter que ler a documentação frequentemente, que pode estar desatualizada ou pior ter que fazer engenharia reversa para entender o que determinada função ou classe faz e como a implementa.
Transporte agnóstico: clientes diferentes, como integração de fila de mensagens ou trabalhos agendados, podem usar o aplicativo diretamente ou por meio de um protocolo de transporte mais apropriado, sem serem forçados a usar HTTP, e ambos (o cliente e o fornecedor do aplicativo) não precisam saber exatamente como aquilo foi implementado e nem como internamente cada um dos lados chegou ao resultado, o que os deixa independentes e permite que ambos evoluam separadamente.
REST
REpresentational State Transfer (REST) (Representação do Estado de Transferência, em tradução livre para o português) é um estilo de arquitetura que define um conjunto de restrições para a criação de APIs da web. O termo foi cunhado por Roy Fielding em sua tese de doutorado e expande muitas das decisões de design que foram feitas no protocolo HTTP para uma arquitetura de nível mais alto. Embora seja mais frequentemente usado no contexto de HTTP, REST é um padrão de design de arquitetura e não um protocolo de comunicação. Como uma implementação da arquitetura REST, o HTTP é considerado um protocolo RESTful.
Resumidamente, REST é nada mais é que um padrão de arquitetura para criar serviços e disponibilizá-los na Web. Já RESTful é um serviço que foi implementado simplesmente fazendo uso deste padrão. As principais características, seriam:
- Cliente-servidor: de um lado temos o servidor trabalhando com os dados e do outro o cliente fazendo suas interações e requisições. Esta separação de responsabilidades deve ser a mais clara e transparente possível.
- Sem estado (stateless): as requisições do cliente para o servidor deve conter todas as informações, ou seja, o servidor não deve manter o estado do cliente (sessão) do seu lado. Desse modo, podendo responder e esquecer aquele cliente, sem se preocupar com as requisições futuras ou serviços futuros.
- Cacheável: as respostas do servidor devem determinar se aquela informação pode entrar ou não em um cache, através do seu estado atual ou de sua conclusão. Assim o cliente pode confiar se aquela resposta pode ser usada novamente em uma requisição equivalente, ou em uma requisição futura, ou mesmo para desencadear outros procedimentos.
- Interface uniforme: este item tem relação com o uso correto dos verbos HTTP GET, POST, PUT, DELETE, entre outros e de uma separação clara dos recursos e de seus níveis, ou seja, um serviço REST não é aquele que simplesmente manda um JSON de um lado para o outro, existem regras definidas para que possa ser mais fácil de ser usada, mais confiável e com melhor performance.
Um exemplo que deixa isto mais claro é quando comparamos dois serviços http onde um é baseado em REST e o outro é baseado em RPC, ambos são uma API de gerencia pedidos.
RPC significa Remote Procedure Call e é uma definição genérica que, basicamente, está relacionada a requisições remotas e síncronas independentes de protocolo, uma simples chamada HTTP sem observar qualquer padrão já pode ser considerada uma chamada RPC, sendo assim, a melhor maneira que vejo para explicar o que é REST é mostrando um exemplo bem distante do REST. Vamos imaginar que vamos criar um novo pedido usando um POST em uma API RPC, mesmo usando o formato JSON, temos:
POST /pedidos (INSERIR UM PEDIDO) { "NovoPedido": { "nome": "Nome do Pedido", "valor": "9.99", "numero": "1001" } } POST /pedidos (BUSCAR DADOS DO PEDIDO) { "BuscarPedido": { "numero": "1001" } } POST /pedidos (REMOVER O PEDIDO) { "RemoverPedido": { "numero": "1001" } }
Note que todas usaram o mesmo método POST, o que diferenciou foi, que passamos parâmetros diferentes para cada procedimento. Não está claro na API que temos diferentes formas de alterar o estado do recurso “pedido”. Vamos ver agora, como ficaria o mesmo exemplo baseando-se no REST, porém, sem implementa-lo em sua plenitude:
POST /pedidos (CRIA UM PEDIDO) { "nome": "Nome do Pedido", "valor": "9.99", "numero": "1001" } Buscar o pedido pelo id nem precisaria de um body na requisição, só usaríamos as informações presente na própria URL, passando o identificador do pedido: GET /pedidos/1001 E remover o pedido seria parecido com o GET: DELETE /pedidos/1001 Cada um dos serviços acima também retornaria o status HTTP correspondente. No caso do POST seria um 201 CREATED (que significa "sucesso, recurso foi criado"). No caso do GET seria um 200 OK ("sucesso"). No caso do DELETE seria um 204 NO CONTENT ("sucesso, sem conteúdo para retornar como resposta") e assim por diante.
E por qual motivo a API acima é “baseada” em REST e não RESTFul? A maioria dos desenvolvedores acredita que uma API RESTFul para no exemplo acima, mas para ser considerada RESTFul ela deveria também aplicar o HATEOAS, que vem de Hypermedia As the Engine Of Application State e o termo “hipermídia” no seu nome já dá uma ideia de como este componente funciona em em uma aplicação RESTful, que ao ser implementado, a propria API passa a fornecer links que indicarão aos clientes como navegar através dos seus recursos.
Creio que esta polêmica de definir o que é uma API REST ou não acabou gerando níveis de maturidade de uma API Rest, dada a dificuldade (ou até, pouca necessidade) de aplicar o HATEOAS nas APIs. Estes níveis de maturidade podem auxiliar os desenvolvedores a entenderem que ponto suas APIs estão no momento e até onde podemos chegar dada as necessidades da API a ser desenvolvida. Isso é sem dúvida nenhuma assunto para outros diversos artigos diferentes…
Restrições
Como não é um protocolo definido formalmente, existem muitas opiniões sobre os detalhes da implementação de APIs REST. No entanto, as cinco restrições a seguir devem estar presentes para que qualquer aplicativo seja considerado RESTful:
A arquitetura deve ser cliente-servidor, pois permite uma separação clara de interesses de cada um dos lados. O cliente é responsável por solicitar e exibir os dados enquanto o servidor cuida do armazenamento dos dados e da lógica do aplicativo e do negócio. Uma vantagem é que ambos os lados podem ser desenvolvidos separadamente, desde que o formato de solicitação acordado seja seguido.
A comunicação entre o cliente e o servidor é sem estado, ou seja, cada solicitação do cliente contém todas as informações necessárias para o servidor processar a solicitação, o que reduz a complexidade do lado do servidor, pois nenhum estado global (exceto o banco de dados, que possivelmente é compartilhado, porém, de conhecimento somente do servidor) é necessário e melhora a escalabilidade, pois qualquer solicitação pode ser processada por qualquer servidor, e em qualquer tempo. O cliente não precisa saber se os dados estão em um único lugar, em vários, qual a versão do software instalado, qual o sistema operacional entre outros detalhes.
A comunicação cliente-servidor sem estado pode aumentar a carga do servidor, pois algumas informações podem ter que ser transferidas várias vezes, de modo que as solicitações que apenas recuperam dados devem ser armazenadas em cache, e o estado do cache pode ser informado na própria resposta, implementada através de cabeçalhos e resposta definidos no protocolo http
No contexto REST, um cliente não pode necessariamente dizer se está se comunicando diretamente com o servidor ou com um intermediário (proxy) isso permite uma maior escalabilidade dos serviços e maior facilidade de distribuição, até mesmo podendo existir servidores diferentes para diferentes recursos, ou servidores intermediários para alivio de carga.
Recursos e Forma de Operação
Um dos principais conceitos de REST e do proprio protocolo HTTP são os recursos, que na verdade podem ser qualquer coisa diferençável ou endereçada de forma exclusiva. Por exemplo, esta postagem do blog, uma lista de todas as minhas postagens, um arquivo de um pdf em um servidor, uma entrada em um banco de dados ou os dados meteorológicos para um determinado local, tudo isso são recursos diferentes que podem ser endereçados em um servidor usando o protocolo HTTP e definem um conjunto de operações que podem ser executadas em um ou vários recursos de maneira simultânea ou não.
O conceito de recursos e operações está definido na especificação HTTP e curiosamente, é algo em que poucos frameworks tentam ajudar. Em vez de ajudar a lidar com recursos e essas operações, eles focam no tratamento de baixo nível de solicitações e respostas HTTP individuais (roteamento, cabeçalhos etc.) ou forçam o protocolo HTTP orientado a recursos e suas operações em ações arbitrárias (controladores). Eu ainda acredito que as inconsistências em muitas APIs da web que afirmam ser RESTful não são um problema de REST como uma arquitetura, mas de frameworks que não fornecem a estrutura para segui-la adequadamente.
Alguns recursos, ou verbos HTTP como são conhecidos, são amplamente utilizados e conhecidos, porém, uma infinidade de outros está implementada e futuros podem ser acrescidos até mesmo pelos próprios desenvolvedores para acomodar suas necessidades, vamos falar um pouco sobre eles:
GET: É um método seguro somente de leitura, e que lê um único recurso ou uma lista de recursos, dependendo dos seus parâmetros ou do endpoint solicitado.
POST: Cria um novo recurso, onde o body da requisição deve conter TODOS os dados ou os dados obrigatórios e opcionais definidos.
PUT: Substitui completamente o recurso, informando novos dados, ou seja, ele é muito similar ao POST, porém, substitui um recurso já existente, seria como chamar um DELETE, seguido de um POST, e deve conter TODOS os dados do registro, sejam ele somente os obrigatórios ou também os não obrigatórios.
PATCH: Mescla o recurso com os novos dados, ou seja, ele atualiza uma parte do recurso existente, podendo ou não sobrepor totalmente o recurso existente, se você passar somente alguns dados, eles serão acrescidos ou substituídos pelo conteúdo total.
DELETE: Exclui o recurso do local, tornando-o indisponível.
Além destes já citados, alguns outros foram sendo incluidos para atender a todas as necessidades, e você também pode criar um novo de acordo com a necessidade de sua requisição, ou dos recursos que você necessitar em sua API ou APLICAÇÃO, são alguns mais não todos os seguintes:
HEAD: O mesmo que GET, porém, retorna apenas o cabeçalho e nenhum dado, ou seja, se o recurso existe e se está disponível, útil, para evitar que grandes respostas sejam transferidas sem necessidade, ou simplesmente para verificar se já existe.
COPY: Copia o recurso, criando outro registro idêntico, ou somente com o ID diferente. Seria como chamar 2 vezes o mesmo POST, algumas APIs podem preferir implementar desse modo a duplicação, para evitar duplicidade de dados, por repetidas chamadas a POST.
OPTIONS: Usado para listar as opções de comunicação com o servidor e o recurso, geralmente implementado para servir como um guia de implementação, ele pode ser usado para listar os verbos disponíveis e suas utilidades ou formas de serem chamados.
LINK e UNLINK: Respectivamente servem para vincular e desvincular dois recursos, por exemplo, você tem uma série de notas, que queria vincular a um aluno, você poderia implementar o LINK para criar e validar essa forma de implementação, e usaria o unlink para desvincular ou eliminar a sequencia de notas, enfim, uma série de usos podem fazer uso desse verbo.
PURGE: Usado por exemplo para controle de cache de uma requisição, quando você precisa por exemplo eliminar um recurso do cache, ou em algumas implementações, você pode eliminar um recurso, simplesmente marcando-o para deleção, e ao executar o PURGE, você fisicamente elimina o recurso, outra especificação que está prevista, porém, nem todos os serviços implemental.
LOCK e UNLOCK: Usado para sinalizar uma edição por um PUT ou PATCH, você pode implementar este recurso para evitar que durante uma edição, dados sejam alterados por fontes diferentes, por exemplo, você pode necessitar implementar este verbo, caso algum processo demorado possa ter sido iniciado em um recurso, e precisar travar este recurso para edição ou exclusão, por exemplo para não destruir uma interdependência. Respectivamente o UNLOCK serve para implementar uma forma de destravar um recurso, que estava previamente travado.
PROPFIND: Usado para implementar métodos de pesquisa de recurso específico, retornando simplesmente se foi ou não localizado.
SEARCH: Também usado para pesquisas mais amplas, retornando os possíveis recursos que possam atender a solicitação.
HELP: Usado por algumas implementações, para apresentar um recurso de tutorial, ou uma hiperligação para ajuda estecífica sobre o recurso solicitado, ou alguns outros detalhes.
Em última análise, a RFC responsável pela implementação e documentação desses verbos, especifica um conjunto com 39 possibilidades diferentes, e ainda deixa em aberto a possibilidade de serem adicionados novos ou removidos de acordo com as necessidades.
Camada de Serviços REST
Na seção da camada de serviço, examinamos as vantagens que ele pode trazer para testabilidade, independência de protocolo e auto documentação. Isso agora pode ser combinado com as restrições REST de recursos e uma interface uniforme para criar um serviço independente de protocolo que espelha os métodos HTTP.
Ele nos dá uma abstração intuitiva, mas poderosa, para lidar com quase todos os tipos de dados. Essa interface nos permite implementar nossa lógica de aplicativo de uma forma orientada a recursos com todas as vantagens discutidas na camada de serviço e nas seções REST. Embora reflita diretamente os métodos HTTP, é independente do protocolo e não precisamos escrever manipuladores de solicitação e resposta HTTP individuais. Em vez disso, isso pode ser feito em um manipulador separado que só precisa saber sobre essa interface de serviço.
Outra vantagem dessa abordagem é que podemos adicionar manipuladores ou verbos para outros protocolos sem precisar alterar nossos serviços. Websockets, por exemplo, não pode apenas enviar eventos do servidor para o cliente. A maioria das bibliotecas de websocket permite comunicação totalmente bidirecional que pode substituir completamente o HTTP e, muitas vezes, também é mais rápida.
Middleware
Embora a camada de serviço RESTful nos forneça uma interface elegante para criar APIs web, a maioria dos aplicativos também requer funcionalidade que não faz parte da responsabilidade central dos serviços ou que se aplica a vários serviços, ou questões transversais, como por exemplo a verificação de permissões, validação e conversão de dados, envio de email e de notificações para os responsáveis quando um novo usuário é criado ou atualizar as informações de estoque quando um pedido foi enviado enfim, regras para o perfeito funcionamento, porém não diretamente ligadas aos recursos.
A solução tem muitos nomes diferentes: Middleware, Unix pipes, Aspect Oriented Programming, Feathers hooks, mas tudo se resume na mesma coisa, o que precisamos é uma forma de registrar a funcionalidade que é executada antes ou depois de um método. Ele deve ter acesso ao contexto da chamada do método e pode decidir quando e se deseja passar o controle para a próxima etapa. Ele também pode executar outras funcionalidades, uma vez que todos os outros middleware foram concluídos por exemplo, para registrar ou adicionar informações ao resultado.
Na Programação Orientada a Aspectos, que permite adicionar funcionalidades adicionais às classes, é feito em tempo de compilação. Para linguagens dinâmicas, pode ser um pouco mais flexível estendendo os métodos em tempo de execução. Um padrão usado para isso em linguagens que permitem uma abordagem mais funcional é o chamado estilo de passagem de continuação.
Na programação funcional, o estilo de passagem de continuação (CPS) é um estilo de programação no qual o controle é passado explicitamente na forma de uma continuação. Esta definição como vista na Wikipedia pode soar um pouco abstrata, mas é muito comum, especialmente em NodeJS, onde é conhecida como middleware e popularizada por estruturas da web como Express e Koa. A ideia é ter camadas de manipuladores de solicitação / resposta HTTP em que cada um faça algo específico e possam decidir quando passar o processamento para o próximo manipulador. Por exemplo.
Embora mais popular para lidar com solicitações HTTP, esse padrão geralmente é muito útil para qualquer coisa que requeira um fluxo de trabalho de processamento assíncrono configurável. Aplicado à nossa camada de serviço RESTful, podemos registrar o mesmo tipo de middleware para cada método. Em vez da solicitação ou resposta HTTP no contexto, ele contém informações independentes de protocolo sobre os parâmetros (por exemplo, id, dados ou parâmetros) e como foi chamado (por exemplo, o nome do método e o objeto de serviço).
Podemos ter também, atualizações em tempo real que significam que os clientes são notificados ativamente sobre as mudanças no sistema. Não faz parte da arquitetura REST ou do protocolo HTTP, mas se encaixa quase que naturalmente no conceito de interface uniforme. Como conhecemos os efeitos colaterais de cada método, podemos enviar automaticamente certas notificações assim que forem concluídas. Os clientes podem então ouvir os eventos nos quais estão interessados e atualizar seu estado de acordo. Isso também se encaixa bem na camada de serviço RESTful.
O envio desses eventos pode ser implementado como apenas outro middleware que é executado por último e publica o resultado final. Mesmo que o HTTP não suporte atualizações em tempo real, os clientes que usam outros protocolos (como websockets) também podem ser notificados sobre esses eventos por meio de seu manipulador de protocolo. Este é um excelente meio-termo entre eventos de websocket totalmente personalizados e soluções proprietárias em tempo real. Com eventos totalmente personalizados, cabe ao desenvolvedor saber o que o evento significa e fazer as atualizações apropriadas, enquanto soluções prontas para usar como Firebase ou Meteor usam protocolos em tempo real que são difíceis de usar diretamente e geralmente requerem bibliotecas específicas do lado do cliente.
Com eventos de serviços RESTful, sabemos quais eventos obteremos e quais dados esperar. Isso nos permite criar ferramentas genéricas sem a necessidade de implementar um protocolo de dados em tempo real complexo. Esses eventos combinam especialmente bem com a programação reativa funcional (FRP) para criar interfaces de usuário com base em fluxos de dados em tempo real. Pretendo discutir isso em um artigo futuro.
Concluindo
Os padrões de projeto são as melhores práticas que podem nos ajudar a criar software sustentável, flexível e mais fácil de entender, independentemente da linguagem de programação ou estrutura. Nesta postagem, vimos vários padrões de design e arquitetura que podem ajudar a criar APIs da web como Camada de serviço: uma interface independente de protocolo para nossa lógica de aplicativo. REST: um princípio de design arquitetônico para a criação de APIs da web. Serviços RESTful: uma camada de serviço que segue a arquitetura REST e os métodos de protocolo HTTP. Middleware: funções reutilizáveis que podem controlar o fluxo de dados e acionar funcionalidades adicionais ao interagir com os serviços REST. Tempo real: um conjunto de eventos que podem ser enviados automaticamente ao seguir a arquitetura REST.
Combinados, eles nos permitem criar APIs da web que são mais fáceis de entender e manter por meio de uma interface de serviço comum, mais flexível, e preparada para o futuro por ser agnóstica de protocolo e de linguagem e amigável para o usuário e para o desenvolvedor. Acredito que em um futuro próximo, especialmente as abordagem independente de protocolo, independente de linguagem e de arquitetura computacional, e o mais importante em tempo real serão muito importante para o futuro das aplicações conectadas e desenvolvimento voltado a web.