Generalizando endpoints

É muito fácil descrever uma API HTTP:

GET /users/:id

Um verbo HTTP e um padrão do caminho é tudo o que é necessário. Chamamos essa conjunção de endpoint. Embora o nome seja mais antigo que REST e estivesse em uso para serviços da Web XML-RPC e SOAP, REST o trouxe claramente para o mercado principal.

Endpoints provaram ser muito bem-sucedidos em lidar com alguns desafios padrão de arquiteturas distribuídas:

  1. Clareza: eles fornecem uma representação clara e fácil de compartilhar da interface disponível.
  2. Semântica: aproveitando os idiomas HTTP, eles permitem uma representação estruturada do domínio do aplicativo. Isso é especialmente verdadeiro ao expor operações CRUD nos recursos do aplicativo. O resultado é uma API muito comunicativa que pode ser apreendida intuitivamente.

Endpoints e protótipos de função

Os endpoints podem ser comparados a protótipos de funções. Pegue estas duas interfaces:

getUser(id)

GET
/users/:id

Se você pensar na getUserfunção como um padrão de string, poderá facilmente ver que ambos são conceitualmente equivalentes. Se a função for consumida em uma linguagem de script, o paralelo é ainda mais próximo, pois o nome da função pode ser construído em tempo de execução.

Eles também são muito semelhantes em termos de funcionalidade:

  • Ambos têm um nome descritivo
  • Ambos têm um conjunto de argumentos possíveis
  • Ambos fornecerão algum tipo de valor de retorno

Endpoints são muito mais expressivos do que nomes de métodos regulares. Isso se deve aos recursos do HTTP que permitem representar diferentes tipos de interações. A observação desse paralelo me fez colocar a questão: os pontos finais não são algum tipo de generalização de protótipos de função?

Não é possível levar a generalização de protótipos de função à conclusão lógica?

Chamadas de método como objetos semelhantes a eventos

Outro paralelo que pode ser traçado é entre chamadas de método e emissões de eventos:

Target.write(text)

Target.emit("write", text)

A principal diferença entre chamadas de métodos regulares e emissões de eventos é que muitos ouvintes podem ser acionados pelo evento, enquanto apenas uma implementação do método é permitida. É óbvio agora como os eventos nada mais são do que uma aplicação generalizada do mesmo conceito central dos métodos.

Vamos ilustrar isso melhor. Podemos reescrever a chamada do método, enviando um objeto semelhante a um evento para um executemétodo imaginário na biblioteca central da linguagem:

Core.execute({
method
: "write",
object
: Target,
arguments
: [ text ]
})

Há algumas coisas faltando aqui, como o escopo em que a execução acontece e a pilha de chamadas, mas você entendeu. No final, implementar um método pode ser visto como nada mais do que adicionar um retorno de chamada a um evento que é descrito não apenas por um nome, mas por uma combinação de propriedades do referido evento.

A generalização

O resultado deste exercício foi construir a biblioteca Object Pattern e a Object Pattern Notation que a acompanha. Embora seja possível implementar algo ao longo das linhas do método acima, ele não tem o tipo de flexibilidade que eu estava procurando. Imaginei que, uma vez que já estamos usando endpoints REST com sucesso, implementar uma generalização do conceito de endpoint seria uma primeira etapa crucial para obter uma arquitetura orientada a eventos generalizada.Core.run

Se estiver interessado, você deve definitivamente verificar o playground do Object Pattern para obter mais informações sobre como usá-los. Vou folhear a superfície para explicar como uma implementação de um recurso de aplicativo (uma biblioteca de gerenciamento de usuário generalizado, por exemplo) seria semelhante nesta arquitetura. Usarei expressões HTTP porque são familiares, mas tenha em mente que os Padrões de Objeto são projetados para descrever qualquer tipo de estrutura de objeto.

Core.attach({
"resource:/users/**": {
"method:GET": function (event) {
event
//=> { method: "GET", resource: ["users", 24] }

Core.emit({
method
: "PUT",
resource
: ["users", 24],
body
: { name: "Jennifer" }
})
},

"method:POST": function (event) {
event
//=> { method: "POST", resource: ["users"], body: { name: "John" } }
}
}
})

Core.emit({
method
: "POST",
resource
: ["users"],
body
: {
name
: "John"
}
})

Na vida real, a arquitetura também precisaria de uma maneira de definir o escopo das transações (de modo que, quando você emite algum evento na esperança de obter uma resposta, possa identificar qual evento deveria ser a sua resposta), mas como você pode ver, abstraindo terminais, chegamos a uma generalização de vários paradigmas de programação:

  • Generalização de OOP: recursos atuam como objetos que mantêm coleções de callbacks que, por sua vez, ouvem padrões específicos em vez de nomes de métodos específicos. Os próprios eventos também são objetos, mas não têm tipo, pois o que distingue um evento de outro são as propriedades que ele contém (e, consequentemente, os callbacks que ele irá disparar). Outra maneira de ver isso é que os métodos disponíveis para um objeto de evento são o conjunto de funções que ouvem as propriedades desse objeto de evento específico. Objetos de evento são mensagens e modelos de dados. Essa duplicidade de funções é, na verdade, um dos objetivos da arquitetura.
  • Generalização de arquiteturas assíncronas orientadas a eventos: as execuções são eventos, mas os eventos não têm como escopo um emissor de evento específico e também não são nomeados ou digitados. Além disso, não há diferença entre o nome do evento e os argumentos enviados para o retorno de chamada: muito parecido com um clickevento de UI , o evento contém referências a todo o seu contexto.
  • Generalização de aplicativos no aplicativo vs aplicativos em rede: dado que a API é consumida por meio de objetos de evento e as respostas enviadas também na forma de objetos de evento, não há diferença na interface para consumir um recurso no mesmo tempo de execução do aplicativo ou via uma rede. Isso é excelente, pois a escalabilidade exige que as peças do aplicativo possam ser retiradas e isoladas com o mínimo de atrito possível.

Abstração como o caminho natural do progresso

A ideia de objetos com os quais você pode interagir de maneira uniforme e que possuem propriedades específicas é um poderoso modelo de realidade e que impulsionou a programação por um longo tempo.

A ideia de ações acontecendo como (mais ou menos) eventos globais aos quais vários assinantes podem reagir é outra ideia poderosa que mais recentemente ajudou na construção de aplicativos complexos com várias partes, como GUIs.

A ideia de que os dados podem ser enviados como mensagens em filas e processados ​​continuamente é outra grande ideia, que impulsionou os serviços em tempo real nos últimos anos.

Mas colocar todas essas ideias juntas em um único aplicativo pode se tornar uma bagunça gigante. Projetar um aplicativo com os paradigmas concorrentes lado a lado faz com que você gaste muito tempo debatendo questões como:

  • Deve ser um método ou deve ser um evento? Devo torná-lo um evento de consistência, mesmo quando haverá apenas um retorno de chamada?
  • Esta função sempre será síncrona? Devo implementar isso como uma promessa ou deve ter um valor de retorno?

A abstração é o caminho de menor resistência quando os paradigmas convergem. Uma abstração que pode abranger as abordagens atuais provavelmente ajudará no esforço de construir a próxima iteração de programação.