Classificar o caos

Como mostra a prática, uma grande parte dos problemas surge não por causa das próprias soluções, mas por causa de como ocorre a comunicação entre os componentes do sistema. Se houver uma confusão na comunicação entre os componentes do sistema, como você não tenta escrever bem os componentes individuais, o sistema como um todo falhará.


Cuidado Dentro da bicicleta.


Problema ou declaração do problema


Há algum tempo, passou a trabalhar em um projeto para uma empresa que traz para as massas delícias como CRM, sistemas ERM e derivativos. Além disso, a empresa emitiu um produto bastante abrangente, de software para caixas registradoras ao call center, com a possibilidade de alugar operadores no valor de até 200 almas.


Eu próprio trabalhei em um aplicativo front-end para call center.


É fácil imaginar que são informações de todos os componentes do sistema que fluem para o aplicativo do operador. E se levarmos em conta o fato de que não é um único operador, mas também um gerente e administrador, você pode imaginar quanta comunicação e informação o aplicativo deve "digerir" e se relacionar.


Quando o projeto já foi lançado e até funcionou de maneira bastante estável, o problema da transparência do sistema surgiu em todo o seu crescimento.


Este é o ponto. Existem muitos componentes e todos eles trabalham com suas fontes de dados. Mas quase todos esses componentes foram escritos como produtos autônomos. Ou seja, não como um elemento do sistema geral, mas como decisões separadas para venda. Como resultado, não há uma API (sistema) única e nem padrões de comunicação comuns entre eles.


Eu vou explicar Algum componente envia JSON, "alguém" envia linhas com a chave: valor interno, "alguém" envia binário em geral e faz o que quiser com ele. Mas, e o pedido final para o call center precisava obter tudo e processá-lo de alguma forma. Bem, e mais importante, não havia um link no sistema que pudesse reconhecer que o formato / estrutura dos dados foi alterado. Se algum componente enviou JSON ontem e hoje decidiu enviar binário - ninguém verá isso. Somente o aplicativo final começará a falhar conforme o esperado.


Logo ficou claro (para aqueles ao meu redor, não para mim, desde que falei sobre o problema no estágio de design) que a ausência de uma “linguagem única de comunicação” entre os componentes leva a problemas sérios.


O caso mais simples é quando o cliente solicita a alteração de algum conjunto de dados. Eles cancelam a tarefa para o jovem que “detém” o componente para trabalhar com bancos de dados de bens / serviços, por exemplo. Ele faz seu trabalho, implementa um novo conjunto de dados e, para ele, imbecil, tudo funciona. Mas, no dia seguinte à atualização ... ah ... o aplicativo no call center repentinamente começa a funcionar como não esperava.


Você provavelmente já adivinhou. Nosso herói alterou não apenas o conjunto de dados, mas também a estrutura de dados que seu componente envia ao sistema. Como resultado, o aplicativo de call center simplesmente não pode mais trabalhar com esse componente e outras dependências voam pela cadeia.


Eles começaram a pensar sobre o que realmente queremos sair. Como resultado, formulamos os seguintes requisitos para uma solução em potencial:


Em primeiro lugar: qualquer alteração na estrutura de dados deve ser imediatamente "destacada" no sistema. Se alguém fez alterações em algum lugar e essas alterações são incompatíveis com o que o sistema espera, um erro deve ocorrer no estágio de teste do componente, que foi alterado.


O segundo Os tipos de dados devem ser verificados não apenas durante a compilação, mas também em tempo de execução.


O terceiro . Como um grande número de pessoas com níveis de habilidade completamente diferentes trabalha com componentes, a linguagem de descrição deve ser mais simples.


Quarta . Qualquer que seja a solução, deve ser o mais conveniente possível trabalhar com ela. Se possível, o IDE deve destacar o máximo possível.


O primeiro pensamento foi implementar o protobuf. Simples, legível e fácil. Digitação estrita de dados. Parece ser o que o médico pediu. Mas, infelizmente, nem toda sintaxe do protobuf parecia simples. Além disso, mesmo o protocolo compilado exigia uma biblioteca adicional, mas o Javascript não era suportado pelo protobuf e era o resultado do trabalho da comunidade. Em geral, eles recusaram.


Então surgiu a ideia de descrever o protocolo em JSON. Bem, quanto mais fácil?


Bem, então eu parei. E sobre isso, este post poderia ter sido concluído, pois depois da minha partida ninguém mais começou a lidar particularmente com o problema.


No entanto, considerando alguns projetos pessoais em que a questão da comunicação entre componentes voltou a atingir todo o seu potencial, decidi começar a implementar a ideia por conta própria. O que será discutido abaixo.


Portanto, apresento a sua atenção o projeto ceres , que inclui:


  • gerador de protocolo
  • fornecedor
  • o cliente
  • implementação de transportes

Protocolo


A tarefa era fazer com que:


  • foi fácil definir a estrutura da mensagem no sistema.
  • foi fácil determinar o tipo de dados de todos os campos da mensagem.
  • foi possível definir entidades auxiliares e fazer referência a elas.
  • e, é claro, para que tudo isso seja destacado pelo IDE

Penso que, de uma maneira completamente natural, como uma linguagem na qual o protocolo é convertido, o TypeScript foi escolhido não como Javascript puro. Ou seja, tudo o que o gerador de protocolo faz é transformar JSON em Typescript.


Para descrever as mensagens disponíveis no sistema, você só precisa saber o que é JSON. Com o qual, tenho certeza que ninguém tem nenhum problema.


Em vez de Hello World, ofereço um exemplo não menos hackney - chat.


{ "Events": { "NewMessage": { "message": "ChatMessage" }, "UsersListUpdated": { "users": "Array<User>" } }, "Requests": { "GetUsers": {}, "AddUser": { "user": "User" } }, "Responses": { "UsersList": { "users": "Array<User>" }, "AddUserResult": { "error?": "asciiString" } }, "ChatMessage": { "nickname": "asciiString", "message": "utf8String", "created": "datetime" }, "User": { "nickname": "asciiString" }, "version": "0.0.1" } 

Tudo é escandalosamente simples. Temos alguns eventos NewMessage e UsersListUpdated; bem como algumas solicitações UsersList e AddUserResult. Existem mais duas entidades: ChatMessage e User.


Como você pode ver, a descrição é bastante transparente e compreensível. Um pouco sobre as regras.


  • Um objeto em JSON se tornará uma classe no protocolo gerado
  • O valor da propriedade é uma definição de tipo de dados ou uma referência a uma classe (entidade)
  • Objetos aninhados do ponto de vista do protocolo gerado se tornarão classes "aninhadas", ou seja, objetos aninhados herdarão todas as propriedades de seus pais.

Agora tudo que você precisa fazer é gerar um protocolo para começar a usá-lo.


 npm install ceres.protocol -g ceres.protocol -s chat.protocol.json -o chat.protocol.ts -r 

Como resultado, obtemos um protocolo gerado pelo TypeScript. Conectamos e usamos:


imagem

Portanto, o protocolo já fornece algo para o desenvolvedor:


  • O IDE destaca o que temos no protocolo. O IDE também destaca todas as propriedades esperadas.
  • Texto datilografado, que certamente nos dirá se algo está errado com os tipos de dados. Obviamente, isso é feito no estágio de desenvolvimento, mas o próprio protocolo já verifica os tipos de dados em tempo de execução e gera uma exceção se uma violação for detectada
  • Em geral, você pode esquecer a validação. O protocolo fará todas as verificações necessárias.
  • O protocolo gerado não requer nenhuma biblioteca adicional. Tudo o que ele precisa para trabalhar, ele já contém. E é muito conveniente.

Sim, o tamanho do protocolo gerado pode surpreendê-lo, para dizer o mínimo. Mas, não se esqueça da minificação, à qual o arquivo de protocolo gerado se presta bem.

Agora podemos "embalar" a mensagem e enviar


 import * as Protocol from '../../protocol/protocol.chat'; const message: Protocol.ChatMessage = new Protocol.ChatMessage({ nickname: 'noname', message: 'Hello World!', created: new Date() }); const packet: Uint8Array = message.stringify(); // Send packet somewhere 

É importante fazer uma reserva aqui, o pacote será uma matriz de bytes, o que é muito bom e correto do ponto de vista da carga de tráfego, pois o envio dos mesmos "custos" JSON, é claro, é mais caro. No entanto, o protocolo possui um recurso - no modo de depuração, ele gera JSON legível para que o desenvolvedor possa "olhar" para o tráfego e ver o que acontece.


Isso é feito diretamente no tempo de execução.


 import * as Protocol from '../../protocol/protocol.chat'; const message: Protocol.ChatMessage = new Protocol.ChatMessage({ nickname: 'noname', message: 'Hello World!', created: new Date() }); // Switch to debug mode Protocol.Protocol.state.debug(true); // Now packet will be present as JSON string const packet: string = message.stringify(); // Send packet somewhere 

No servidor (ou em qualquer outro destinatário), podemos facilmente descompactar a mensagem:


 import * as Protocol from '../../protocol/protocol.chat'; const smth = Protocol.parse(packet); if (smth instanceof Error) { // Oops. Something wrong with this packet. } if (Protocol.ChatMessage.instanceOf(smth) === true) { // This is chat message } 

O protocolo suporta todos os principais tipos de dados:


TipoValoresDescrição do produtoTamanho, bytes
utf8StringString codificada em UTF8x
asciiStringstring ascii1 caractere - 1 byte
int8-128 a 1271
int16-32768 a 327672
int32-2147483648 a 21474836474
uint80 a 2551
uint160 a 655352
uint320 a 42949672954
float321,2x10 -38 a 3,4x10 384
float645,0x10 -324 a 1,8x10 3088
booleano1

Dentro do protocolo, esses tipos de dados são chamados primitivos. No entanto, outro recurso do protocolo é que ele permite adicionar seus próprios tipos de dados (chamados "tipos de dados adicionais").


Por exemplo, você provavelmente já percebeu que o ChatMessage possui um campo criado com um tipo de dados datetime . No nível do aplicativo - esse tipo corresponde a Data e, dentro do protocolo, é armazenado (e enviado) como uint32 .


Adicionar seu tipo ao protocolo é bastante simples. Por exemplo, se queremos ter um tipo de dados de email , diga a seguinte mensagem no protocolo:


 { "User": { "nickname": "asciiString", "address": "email" }, "version": "0.0.1" } 

Tudo que você precisa fazer é escrever uma definição para o tipo de email.


 export const AdvancedTypes: { [key:string]: any} = { email: { // Binary type or primitive type binaryType : 'asciiString', // Initialization value. This value is used as default value init : '""', // Parse value. We should not do any extra decode operations with it parse : (value: string) => { return value; }, // Also we should not do any encoding operations with it serialize : (value: string) => { return value; }, // Typescript type tsType : 'string', // Validation function to valid value validate : (value: string) => { if (typeof value !== 'string'){ return false; } if (value.trim() === '') { // Initialization value is "''", so we allow use empty string. return true; } const validationRegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/gi; return validationRegExp.test(value); }, } }; 

Isso é tudo. Ao gerar o protocolo, obtemos suporte para o novo tipo de dados de email . Quando tentamos criar uma entidade com o endereço errado, obtemos um erro


 const user: Protocol.User = new Protocol.User({ nickname: 'Brad', email: 'not_valid_email' }); console.log(user); 

Ah ...


 Error: Cannot create class of "User" due error(s): - Property "email" has wrong value; validation was failed with value "not_valid_email". 

Portanto, o protocolo simplesmente não permite que dados "ruins" entrem no sistema.


Observe que, ao definir um novo tipo de dados, especificamos algumas propriedades principais:


  • binaryType - uma referência a um tipo de dados primitivo que deve ser usado para armazenar, codificar / decodificar dados. Nesse caso, indicamos que o endereço é uma string ascii.
  • tsType é uma referência ao tipo Javascript, ou seja, como o tipo de dados deve ser representado no ambiente Javascript. Neste caso, estamos falando de string
  • Também é importante notar que precisamos definir um novo tipo de dados somente no momento da geração do protocolo. Na saída, obtemos um protocolo gerado que já contém um novo tipo de dados.

Você pode ver informações detalhadas sobre todos os recursos do protocolo aqui, ceres.protocol .

Fornecedor e cliente


Em geral, o próprio protocolo pode ser usado para organizar a comunicação. No entanto, se estivermos falando sobre o navegador e o nodejs, o provedor e o cliente estarão disponíveis.


Cliente


Criação


Para criar um cliente, você precisa do cliente e do transporte.


Instalação


 # Install consumer (client) npm install ceres.consumer --save # Install transport npm install ceres.consumer.browser.ws --save 

Criação


 import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ host: 'http://localhost', port: 3005, wsHost: 'ws://localhost', wsPort: 3005, })); // Create consumer const consumer: Consumer = new Consumer(transport); 

O cliente, assim como o provedor, foram projetados especificamente para o protocolo. Ou seja, eles funcionarão apenas com o protocolo (ceres.protocol).

Eventos


Após a criação do cliente, o desenvolvedor pode se inscrever em eventos


 import * as Protocol from '../../protocol/protocol.chat'; import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ host: 'http://localhost', port: 3005, wsHost: 'ws://localhost', wsPort: 3005, })); // Create consumer const consumer: Consumer = new Consumer(transport); // Subscribe to event consumer.subscribe(Protocol.Events.NewMessage, (message: Protocol.Events.NewMessage) => { console.log(`New message came: ${message.message}`); }).then(() => { console.log('Subscription to "NewMessage" is done'); }).catch((error: Error) => { console.log(`Fail to subscribe to "NewMessage" due error: ${error.message}`); }); 

Observe que o cliente chamará o manipulador de eventos apenas se os dados da mensagem estiverem completamente corretos. Em outras palavras, nosso aplicativo está protegido contra dados incorretos e o manipulador de eventos NewMessage sempre será chamado com uma instância de Protocol.Events.NewMessage como argumento.


Naturalmente, o cliente pode gerar eventos.


 consumer.emit(new Protocol.Events.NewMessage({ message: 'This is new message' })).then(() => { console.log(`New message was sent`); }).catch((error: Error) => { console.log(`Fail to send message due error: ${error.message}`); }); 

Observe que não especificamos nomes de eventos em nenhum lugar, simplesmente usamos um link para a classe do protocolo ou passamos uma instância dele.


Também podemos enviar uma mensagem para um grupo limitado de destinatários, especificando um objeto simples do tipo { [key: string]: string } como o segundo argumento. Dentro de ceres, esse objeto é chamado de consulta .


 consumer.emit( new Protocol.Events.NewMessage({ message: 'This is new message' }), { location: "UK" } ).then(() => { console.log(`New message was sent`); }).catch((error: Error) => { console.log(`Fail to send message due error: ${error.message}`); }); 

Assim, ao indicar adicionalmente { location: "UK" } , podemos ter certeza de que apenas os clientes que identificaram sua posição como UK receberão esta mensagem.


Para associar o próprio cliente a uma consulta específica, basta chamar o método ref :


 consumer.ref({ id: '12345678', location: 'UK' }).then(() => { console.log(`Client successfully bound with query`); }); 

Depois de conectar o cliente à consulta , ele tem a oportunidade de receber mensagens "pessoais" ou "de grupo".


Inquéritos


Também podemos fazer pedidos


 consumer.request( new Protocol.Requests.GetUsers(), // Request Protocol.Responses.UsersList // Expected response ).then((response: Protocol.Responses.UsersList) => { console.log(`Available users: ${response.users}`); }).catch((error: Error) => { console.log(`Fail to get users list due error: ${error.message}`); }); 

Vale ressaltar que, como segundo argumento, especificamos o resultado esperado ( Protocol.Responses.UsersList ), o que significa que nossa solicitação será concluída com êxito apenas se a resposta for uma instância da UsersList , em todos os outros casos "cairemos" para pegar Novamente, isso nos garante o processamento de dados incorretos.


O próprio cliente também pode falar com aqueles que podem processar solicitações. Para fazer isso, você só precisa "se identificar" como "responsável" pela solicitação.


 function processRequestGetUsers(request: Protocol.Requests.GetUsers, callback: (error: Error | null, results : any ) => any) { // Get user list somehow const users: Protocol.User[] = []; // Prepare response const response = new Protocol.Responses.UsersList({ users: users }); // Send response callback(null, response); // Or send error // callback(new Error(`Something is wrong`)) }; consumer.listenRequest(Protocol.Requests.GetUsers, processRequestGetUsers, { location: "UK" }).then(() => { console.log(`Consumer starts listen request "GetUsers"`); }); 

Observe que, opcionalmente, como terceiro argumento, podemos especificar um objeto de consulta que pode ser usado para identificar o cliente. Portanto, se alguém enviar uma consulta com consulta , digamos, { location: "RU" } , nosso cliente não receberá essa solicitação, porque sua consulta { location: "UK" } .


Uma consulta pode incluir um número ilimitado de propriedades. Por exemplo, você pode especificar o seguinte


 { location: "UK", type: "managers" } 

Além disso, além de uma correspondência completa da consulta , também processaremos com êxito as seguintes consultas:


 { location: "UK" } 

ou


 { type: "managers" } 

Fornecedor


Criação


Para criar um provedor (assim como criar um cliente), você precisa do provedor e do transporte.


Instalação


 # Install provider npm install ceres.provider --save # Install transport npm install ceres.provider.node.ws --save 

Criação


 import Transport, { ConnectionParameters } from 'ceres.provider.node.ws'; import Provider from 'ceres.provider'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ port: 3005 })); // Create provider const provider: Provider = new Provider(transport); 

A partir do momento em que o provedor é criado, ele pode aceitar conexões de clientes.


Eventos


Assim como o cliente, o provedor pode "ouvir" as mensagens e gerá-las.


Ouvindo


 // Subscribe to event provider.subscribe(Protocol.Events.NewMessage, (message: Protocol.Events.NewMessage) => { console.log(`New message came: ${message.message}`); }); 

Gerar


 provider.emit(new Protocol.Events.NewMessage({ message: 'This message from provider' })); 

Inquéritos


Naturalmente, o provedor pode (e deve) "ouvir" solicitações


 function processRequestGetUsers(request: Protocol.Requests.GetUsers, clientID: string, callback: (error: Error | null, results : any ) => any) { console.log(`Request from client ${clientId} was gotten.`); // Get user list somehow const users: Protocol.User[] = []; // Prepare response const response = new Protocol.Responses.UsersList({ users: users }); // Send response callback(null, response); // Or send error // callback(new Error(`Something is wrong`)) }; provider.listenRequest(Protocol.Requests.GetUsers, processRequestGetUsers).then(() => { console.log(`Consumer starts listen request "GetUsers"`); }); 

Há apenas uma diferença em relação ao cliente, o provedor, além do corpo da solicitação, receberá um clientId exclusivo, atribuído automaticamente a todos os clientes conectados.


Exemplo


Na verdade, eu realmente não quero aborrecê-lo com trechos da documentação, tenho certeza de que será mais fácil e interessante ver apenas um pequeno fragmento de código.


Você pode instalar facilmente o exemplo de bate-papo baixando as fontes e realizando algumas ações simples


Instalação e lançamento do cliente


 cd chat/client npm install npm start 

O cliente estará disponível em http: // localhost: 3000 . Abra imediatamente algumas guias com o cliente para ver a "comunicação".


Instalação e inicialização do provedor (servidor)


 cd chat/server npm install ts-node ./server.ts 

Tenho certeza de que você está familiarizado com o pacote ts-node , mas se não, ele permite que você execute arquivos TS. Se você não deseja instalar, basta compilar o servidor e, em seguida, execute o arquivo JS.


 cd chat/server npm run build node ./build/server/server.js 

O que? Mais uma vez ?!


Antecipando perguntas sobre por que diabos inventar outra bicicleta, porque existem tantas soluções comprovadas, a partir do protobuf e terminando com o joynr hardcore da BMW, só posso dizer que foi interessante para mim. Todo o projeto foi realizado apenas por iniciativa pessoal, sem qualquer apoio, nas horas vagas no trabalho.


É por isso que seu feedback é de particular valor para mim. Em uma tentativa de motivá-lo de alguma forma, posso prometer que, para todas as estrelas no github, vou acariciar o hamster (que, para dizer o mínimo, não gosto). Pelo garfo, uhhh, vou coçar o pussiko dele ... brrrr.


O hamster não é meu, o hamster do filho .


Além disso, em algumas semanas o projeto será testado para meus ex-colegas (que mencionei no início do post e que estavam interessados ​​em qual era a versão alfa). O objetivo é depurar e executar em vários componentes. Eu realmente espero que funcione.


Links e pacotes


O projeto é hospedado por dois repositórios


  • fontes ceres : ceres.provider, ceres.consumer e todos os transportes disponíveis hoje.
  • fontes de gerador de protocolo ceres.protocol

NPM seguintes pacotes disponíveis



Bom e leve.

Source: https://habr.com/ru/post/pt442380/


All Articles