Modelos de programação reativa mental para supervisores


Este artigo é destinado a uma ampla gama de leitores que desejam saber o que é Programação Reativa. O objetivo deste artigo é formar seus modelos mentais básicos de programação reativa (MM RP) sem precisar de detalhes técnicos.

aviso Legal
( ) — , . , .
, : , . , , .
.

Mas primeiro, vamos explicar o que os modelos mentais e superiores mencionados no cabeçalho do artigo têm a ver com isso ...

Sobre modelos mentais
, , , . , .
, , (. [1], [2])
? , , . (), , , . , , «», «» « » .
, , (), , - ().

E aqui estão os chefes ...
. «» «» , : . ( , «» , ).
, «» , , , , , . , . «» «». , , , .
, , , .., () — , , .
, .


Por que a Programação Reativa precisa do seu projeto?


Muitas pessoas que não estão familiarizadas com RP são inicialmente céticas em relação a ele, suspeitando que essa seja apenas outra moda vazia, coberta por algumas palavras bonitas. Especialmente quando eles aprendem que você pode avaliar o PR apenas tentando. E tentar é caro, por causa do alto limite de entrada. Vivemos e vivemos com OOP, o que está faltando?
Deixe-me apresentar meu ponto de vista sobre esse assunto.
No início da programação, quando a maioria dos programas era escrita diretamente em linguagem assembly, o principal conceito de trabalho (um elemento do modelo mental) dos programadores era uma instrução ou comando de linguagem. Alguns dados (primitivos) são alimentados na entrada de um comando ou instrução. A instrução processa e emite alguns dados de saída. O surgimento das primeiras linguagens de programação procedurais, como Fortran, não mudou a essência da questão. Apenas os dados e as operações executadas (como uma sequência de comandos elementares) se tornaram mais complicados.
Com o tempo, ficou claro que esse conceito não é muito consistente com as realidades do mundo. Pode haver muitos dados, eles podem ser difíceis de estruturar. Seria bom dividir os dados e a funcionalidade em torno deles, dividir em partes, desenvolver e manter separadamente e usar juntos.
OOP resolveu esses problemas de várias maneiras. A unidade do Modelo Mental de um programador típico de POO é um objeto com dados ocultos (encapsulados) e uma interface de acesso a esses dados como um conjunto de funções.
OOP teve um papel enorme na automação e informatização de muitos processos de fabricação e outros. E junto com isso, suas fraquezas foram expostas.
Infelizmente, no POO não existe um conceito de processo como tal.
Eles tentaram melhorar a situação de maneiras diferentes, concentrando-se em vários aspectos.
Assim, nasceram a programação orientada a eventos [3], a programação de fluxo de dados [4], o processamento de fluxo [5] e vários outros paradigmas.
Atrevo-me a suscitar uma série de críticas dos adeptos e especialistas sobre esses paradigmas, tentando transmitir em palavras simples sua essência geral.
De uma forma ou de outra, esses paradigmas operam com fluxos de informação. Ao mesmo tempo, a Programação Orientada a Eventos, como o nome sugere, concentra-se no processo de emergência de elementos de fluxo de informações, programação de Fluxo de Dados - no controle de fluxo (divisão, fusão, transformação de fluxos) e processamento de Fluxo no uso ideal de recursos ao processar fluxos.
A programação reativa é praticamente a mesma coisa, mas com foco nas operações elementares específicas de criação, gerenciamento e uso de threads. Essa. O RP descreve como o sistema reage (inglês reage) a elementos do fluxo de informações. Nesse sentido, seria mais correto em russo usar o termo "Programação de reagentes" (da palavra "reagir") ou "Programação de reação" (da palavra "reação a alguma coisa") se não fosse para o ouvido cortar, e o segundo não causou associações incorretas.
Atrevo-me a expressar outro pensamento sedicioso. O que chamamos hoje em inglês de Programação Reativa (Programação Reativa). apelou por razões históricas e inclinou-se a favor deste termo a opinião da maioria.
Esse paradigma poderia ter sido chamado de maneira diferente. Portanto, não se concentre em seu nome atual, mas tente entender sua essência.
E, embora eu fale sobre RP em um nível bastante abstrato, citarei as APIs da biblioteca RxJS como exemplos concretos.
O acrônimo RxJS significa Extensão Reativa para JavaScript, uma extensão JavaScript para os recursos de Programação Reativa. Existem extensões similares para muitas outras linguagens de programação, como pode ser visto na figura abaixo, tirada de [6].
Extensões de programação reativa

Por que os modelos mentais de RP precisam do seu projeto


Grandes projetos não são feitos sozinhos. Muitas vezes, você pode ler ou ouvir que os participantes do projeto devem falar o mesmo idioma. Minha experiência mostra que isso é quase necessário e possível. Mas o que é necessário é que os conceitos mais básicos do projeto sejam declarados e entendidos pelos participantes do projeto da maneira mais igual possível. Em termos de modelos mentais (MM), podemos dizer que os MMs de nível superior devem ser o mais semelhante possível.
Mas como eles podem ser semelhantes se os analistas pensam em termos de fluxo de trabalho e casos de uso, arquitetos em padrões, desenvolvedores em funções e estruturas de dados e testadores em cenários de teste?
Não exorto todos esses especialistas a começarem a pensar ao mesmo tempo com as categorias de Programação Reativa, mas posso prometer a eles que o conhecimento dessas categorias simplificará e aumentará a eficácia de sua comunicação profissional com os colegas. Isso deve acontecer porque, por um lado, os MM RPs têm o poder necessário para descrever fluxos de trabalho complexos e, por outro lado, os MM RPs podem ser convertidos diretamente em código em muitas linguagens de programação.

Surpresas, perigos ou que em RP não é o modo como estamos acostumados


Mas antes de entrarmos na descrição do que consistem os Modelos Mentais de Programação Reativa, com base em nossa própria experiência, gostaria de alertar o leitor sobre o que não existe neles. Além disso, não apenas não, mas a própria expectativa de um comportamento POO simples e compreensível no mundo leva a tristes conseqüências.
Estou fazendo isso não para intimidar, mas para intrigar o leitor.

Diferença 1: em vez de um modelo de cursor, um gráfico computacional


Atrevo-me a sugerir que muitos leitores, ao pensarem na próxima tarefa a ser realizada, tenham um modelo mental na cabeça, que chamo de modelo de cursor. Ele pressupõe que um algoritmo passo a passo na forma de uma lista linear de instruções será inventado para resolver o problema. A execução do algoritmo é reduzida à execução passo a passo das instruções, uma após a outra. Você pode imaginar algo como um ponteiro para a instrução em execução no momento. Depois que a instrução é executada, o ponteiro (cursor) passa para a próxima instrução na lista e começa a executar.
Nesse modelo, uma sequência de comandos escritos em uma linguagem de programação condicional orientada a objetos
1. 1 = 2
2. 2 = 3 
3. 3 = 1 + 2
4.  1, 2, 3
5. 1 = 4
6.  1, 2, 3

vai dar o resultado
2 3 5
4 3 5

Nosso modelo mental do cursor prediz e explica perfeitamente esse resultado. Após o processamento da terceira linha, o valor X3 é definido e o novo valor para X1 especificado na linha 5 não pode alterá-lo.
No mundo do RP, dependendo da interpretação da operação “+”, o resultado provavelmente será esse
2 3 5
4 3 7

Neste mundo, a maioria das operações conecta parâmetros de entrada entre si, criando gráficos computacionais através dos quais os cálculos são "enviados" quando um ou mais parâmetros são alterados.

Diferença 2: Operações assíncronas


Na estrutura do modelo mental de cálculos do cursor, a próxima operação não pode começar mais cedo que a anterior.
Considere o seguinte exemplo. Suponha que a função f1 calcule o salário base pelo valor do identificador de usuário userId, e a função f2 calcule o bônus com base no userId e no valor do salário.
Então, o cálculo do salário total pode ficar assim
1. X = f1(userId)
2. Y = f2(userId, X)
 X, Y

Suponha que um membro da equipe tenha um salário base de 10.000. e um bônus de 1000 unidades.
Nosso modelo mental do cursor indica o que imprimir.
10000 1000 

Infelizmente, no mundo da RP assíncrona, o resultado pode, dependendo da duração das operações, ser
0 0 
10000 0 
0 1000 
10000 1000 

(Ainda não considero exceções).
O fato é que, no mundo reativo assíncrono, a próxima operação não espera o fim da anterior, se for a anterior) assíncrona.
Para ilustrar isso, vejamos alguns detalhes importantes usando o exemplo realista mostrado na figura abaixo.
A imagem mostra o tempo de execução de quatro instruções L1, L2, L3 e L4 que são independentes uma da outra (seus números são importantes para nós, não a ortografia) nos modos síncrono (parte superior da imagem) e assíncrono (parte inferior da imagem).

Como vemos, no primeiro caso, cada instrução subsequente "espera" pelo final da instrução anterior. No caso assíncrono, todas as instruções começam a ser executadas simultaneamente. Devido à execução paralela e ao uso de recursos, a maioria das instruções é executada no modo assíncrono por mais tempo que no modo síncrono. No entanto, juntos eles deixarão seu trabalho mais cedo.
A ordem de conclusão das instruções nos dois modos também é muito diferente. Em sincronia:
L1, L2, L3, l4
mas em assíncrono:
L3, L2, L1, L4
.

Diferença 3: cadeias incompletas (sem consumidor) nem funcionam


Em muitas linguagens de programação tradicionais, é comum associar chamadas de função ou propriedades de objetos a pontos.
Por exemplo, a seguinte cadeia de chamadas de função JavaScript transforma a palavra "good" em "dog":
„good“.split(„“).reverse().join(„“).replace(„oo“, „o“);

Sequências (cadeias) podem ser longas. Por motivos de reutilização ou conveniência, eles podem ser cortados em pedaços e parcialmente executados.
Dividir uma cadeia no RP em duas partes e chamar apenas uma delas geralmente leva a uma falta de resultado, uma vez que apenas a cadeia completa (com o consumidor no final) é executada.

Por que tudo isso?


Provavelmente, muitos leitores já estão se perguntando: “Eles não enlouqueceram coletivamente, esses programadores reativos? Por que é necessário, essa programação? ”
Não presumo prever o que os criadores e especialistas da República da Polônia responderiam a essa pergunta, mas minha resposta é a seguinte: essa programação é necessária, porque muitos objetos do mundo real se comportam exatamente assim.
Gráficos de computação - é nisso que o Excel se baseia, do qual não apenas os contadores, mas também os gerentes de projeto estão satisfeitos.
Operações assíncronas . Quando você faz café ou faz chá na sua cozinha, fica na cozinha todo esse tempo e olha para a sua cafeteira ou bule de chá? Não. O dispositivo ferve a água e faz seu trabalho, enquanto você está fazendo outra coisa por enquanto.
Cadeia completa de operações.Tente desconectar a lâmpada da mesa da tomada e pressionar o interruptor. A lâmpada não acende com isso. Esse objeto funciona apenas se houver uma cadeia completa - da fonte ao consumidor de eletricidade. E existem muitos, se não a maioria, desses objetos no mundo real.

Quero garantir, que seu conhecimento da programação tradicional e do cursor MM não deve ser jogado no lixo devido ao surgimento do RP. A programação reativa os deixou em paz e os expandiu com novas operações em novos tipos de objetos. Como - falaremos sobre isso mais tarde.

O espaço da programação de modelos mentais e o lugar do MM RP nele


Falando sobre o lugar da RP no cenário geral da programação, os autores costumam mencionar duas dimensões - a complexidade dos objetos processados ​​e o sincronismo / assincronia das operações. Um exemplo dessa classificação pode ser encontrado no livro “RxJS in Action” [7], no capítulo “Quando e onde usar o RxJS”.
Nesta classificação, a dimensão dos objetos é dividida em objetos únicos e multi-objetos: matrizes, listas, etc. As operações são divididas em síncronas e assíncronas.
Assim, essa classificação divide o mundo da programação em quatro áreas. O RP é uma dessas áreas responsáveis ​​pelo processamento de vários objetos com operações assíncronas.
Acho essa classificação muito interessante, mas gostaria de analisá-la do ponto de vista dos modelos mentais. A tabela abaixo os apresenta.
Objetos e valores únicos, ,
, (Stream)
, (Promise)(Workflow)

Supomos que os modelos mentais de instruções e o cursor não exijam mais explicações.
O ciclo é uma extensão das instruções MM e do cursor devido às instruções adicionais do ciclo ou retorna a algum ponto. Isso permite que um conjunto de instruções de processamento para um único objeto seja "encapsulado" em um loop e, assim, processe muitos desses objetos. Nesse caso, o cursor se move dentro do ciclo como no modelo anterior e, atingindo o ponto de transição, salta para o início ou o processamento do ciclo para se todos os objetos forem processados.
Jato. A diferença entre esse modelo mental e o anterior é que o cursor
apontando para o objeto processado permanece no lugar e os próprios objetos o "atropelam".
Vejamos isso com dois exemplos. Se você pintar uma cerca de madeira, por analogia com o modelo do cursor, vá de placa em placa. Mas o trabalhador no transportador permanece no local e, por analogia com o modelo de jato, as peças a serem processadas se aproximam dele. Esses objetos são frequentemente referidos pelo termo English Stream, por exemplo, na linguagem Java.
Semáforo. Este MM é mais fácil de associar a um semáforo em um cruzamento. Objetos assíncronos pesquisam periodicamente o estado de uma variável pública e executam determinadas ações após alterar seu estado. (como motoristas na frente de um semáforo)
Aguardando.Uma metáfora adequada para esse Modelo de Expectativa Mental é a carta em papel ou Emall que você esperava na última vez que conseguiu seu emprego. Pode haver uma resposta positiva ou negativa. Seu comportamento depois de receber a carta depende muito do conteúdo. O termo em inglês Promise é frequentemente usado para descrever esses tipos de objetos. Que, do ponto de vista do usuário, é uma expectativa, para o contratado que presta os serviços, é uma promessa.
Como vemos na descrição, o movimento ao longo de cada dimensão (de cima para baixo ou da direita para a esquerda na tabela) significa uma mudança qualitativa no Modelo Mental.
Como pode ser visto na tabela, os jatos e as expectativas são vizinhos à esquerda e no topo da célula do sudeste que nos interessa. O que há de novo nos modelos mentais de fluxos que habitam a célula de interesse para nós em comparação a eles?

Qual é a extensão?


A expansão do Streams em comparação com as Expectativas é que as informações esperadas podem chegar não uma vez, mas em muitas partes. Nesse caso, o processo pode terminar sem terminar. Essa. após uma série de porções bem-sucedidas, receberemos uma notificação de erro. Além disso, outra versão das informações é adicionada - uma notificação do final do processo.
Isso significa, por exemplo, que é possível receber várias (mas não todas) partes das informações esperadas e (sem uma mensagem de erro) uma mensagem sobre o final do processamento.
Lembre-se novamente, com Aguardando, temos apenas duas opções alternativas para as informações resultantes.
O Mental Jet Model é adequado para compreender, discutir e implementar o processo de transformação de uma sequência de objetos do mesmo tipo. O MM Stream o expande com os seguintes aspectos:
  • Pode haver muitos jatos e podemos fundi-los
  • Os jatos podem ser heterogêneos
  • Podemos dividir os jatos em novos de acordo com diferentes critérios
  • Podemos "fechar" e / ou transformá-los em novos dentro da estrutura de um fluxo.

Assim, determinamos o lugar do MM RP (Streams) no espaço ou paisagem dos objetos de Programação. Vamos agora abaixar a vista aérea e dar uma olhada em Streams e seus modelos mentais.

Fluxos e fases do seu ciclo de vida


Como primeira aproximação, os fluxos de RP podem ser imaginados como fluxos de água em tubulações de água ou fluxos de eletricidade. Deve-se lembrar que, como qualquer outra analogia, essa analogia tem seus limites e nem sempre é aplicável.
Falando sobre o fluxo, podemos destacar os seguintes aspectos importantes:

  1. Cada segmento de alguma forma surge
  2. Ele está de alguma forma se movendo em direção ao consumidor.
  3. Algo acontece no caminho com ele (ele se transforma)
  4. Pode ser dividido em vários fluxos ou mesclado com outros fluxos
  5. O consumidor de alguma forma usa o fluxo, deixando de existir.

Os aspectos listados são simultaneamente fases do ciclo de vida de elementos individuais do fluxo.
Vamos considerá-los com mais detalhes usando o exemplo de funções RxJS.

Criação de thread


Os fluxos podem ser criados a partir de elementos passivos, como uma matriz ou uma lista de objetos em seu programa, bytes, linhas de arquivo etc. Esse tipo de fonte de fluxo é chamado frio (embora tecnicamente exista uma definição mais precisa de fontes de fluxo frio).
As chamadas fontes termais "vivem suas próprias vidas" e, se você não se conectar a elas a tempo, as informações serão perdidas. Esta categoria inclui informações sobre ações do usuário em um computador, tablet, smartphone, por exemplo, informações sobre pressionamentos de teclas, movimentos do mouse ou toque na tela. Também nesta categoria estão os dados solicitados por vários protocolos, como HTTP, dados de vários sensores.
Note-se que existem as chamadas fontes “quentes”. Além disso, algumas fontes “quentes” podem ser “resfriadas” e “frias” podem ser “aquecidas”. Mas você deve ler sobre isso na literatura especial, por exemplo, no livro [7].
É importante que saibamos que todas as operações de criação de fluxos criam objetos do mesmo tipo, que podem ser processados ​​pelas mesmas operações, independentemente do conteúdo. Neste artigo, chamamos esses objetos de fluxos. O nome em inglês correspondente é Observável.

Movimento do consumidor e transformação de fluxo


As operações de transformação de fluxo podem ser realizadas tanto no caminho para o consumidor quanto por ele mesmo. Em ambos os casos, as operações de processamento dos elementos de fluxo são estritamente sequenciais, isto é, a próxima operação é lançada estritamente somente depois que a anterior foi concluída e passou seu resultado para ela.
Diferentemente do Stream, que em algumas linguagens de programação são construções de linguagem com sintaxe e semântica próprias, extensões reativas como RxJS em JavaScript são forçadas a usar a sintaxe e a semântica básica de uma linguagem extensível. Portanto, o RxJs implementa a função pipe (), na qual você pode fazer chamadas para funções - manipuladores do próprio fluxo e de seus elementos individuais.
É importante observar que apenas funções especiais, canalizáveis, podem ser manipuladoras de fluxo.

"Fluxo trifásico"


Se continuarmos a analogia com a eletricidade, os fluxos que estamos considerando podem ser chamados de trifásicos. Juntamente com o "fio normal" que transmite as informações básicas, há também um "fio de erro" e um "fio de terminação de fluxo". As operações de transformação permitem não apenas alterar o objeto, mas também redirecioná-lo para outro "fio". Essa técnica é usada, por exemplo, ao processar erros alegados ao trabalhar com servidores usando o protocolo HTTP. Por exemplo, se um servidor não responder, você pode tentar solicitar outro sem informar o usuário sobre a falha ao solicitar o primeiro servidor.
Este é outro elemento muito importante do seu modelo de fluxo mental. Se nos paradigmas de programação tradicionais o erro é retornado da função de processamento como um código de erro ou deve ser interceptado como uma interrupção (exceção), nos fluxos o erro "flui" independentemente do canal principal.
Lá pode ser processado. Por exemplo, se um usuário digitou uma senha incorreta, ele pode ter uma oportunidade adicional de tentar digitá-la uma ou mais vezes.

Dividindo e mesclando fluxos


A divisão dos fluxos é realizada em duas etapas. No primeiro estágio, os threads vazios são iniciados. Em seguida, no segundo estágio (estágio de processamento do fluxo), em uma das funções de processamento, os elementos serão analisados ​​e redirecionados para o fluxo desejado. Tecnicamente, existem muitas opções de como fazer isso. Por exemplo, removendo-o do encadeamento atual ou clonando-o antes de iniciá-lo em um novo encadeamento.
Você pode mesclar vários fluxos em um em um número surpreendentemente grande de maneiras.
As maneiras mais simples que vêm à mente são mesclá-las na ordem de recebimento, ou primeiro todas do primeiro fluxo e depois todas do segundo.
O método mostrado abaixo na figura permite que um dos dois fluxos forme um contendo pares ordenados de objetos dos primeiro e segundo fluxos. Nesse caso, um novo par é formado se um novo elemento aparecer em um dos fluxos. A contém um par estritamente dos últimos elementos de cada fluxo. Isso leva ao fato de que o mesmo elemento pode ser incluído em vários pares.
A notação gráfica usada neste exemplo é chamada de diagramas de mármore e é muito eficaz para explicar a semântica da divisão e mesclagem de fluxos.
Se este tópico lhe interessar, recomendo que você estude as operações e seus diagramas de Marble no recurso [8].


Usando fluxos


Para usar os elementos do fluxo, o usuário ou cliente deve primeiro se inscrever nele. Como regra, no final do processamento, ele deve cancelar sua inscrição, pois os coletores de lixo nem sempre podem desativar automaticamente uma assinatura quando tentam utilizar um assinante.
Muitos clientes podem se inscrever em um thread. Nos RxJs, a função de assinatura é chamada subscribe (). Nele, na maioria dos casos, é aconselhável fazer chamadas de processamento dos elementos "normais" do fluxo, um manipulador de erros e (relativamente raramente) um manipulador de terminação de fluxo.
Cada um dos assinantes do fluxo recebe sua cópia do elemento ou um clone do elemento original. Para não causar problemas, o fluxo é implementado de forma que os elementos recebidos para processamento se tornem imutáveis. Em algumas situações, essa limitação ainda pode ser contornada, mas é melhor não.

Encanto perigoso dos córregos


Os fluxos são objetos muito complicados, um pouco parecidos com as integrais da matemática. Uma coisa é saber que elas existem e até mesmo imaginar o que é, e outra é poder usá-las.
Compreender a lógica interna de seu funcionamento, necessária para aplicá-las bem na prática, requer considerável esforço intelectual.
Os fluxos estão intimamente relacionados à programação funcional. Para o uso competente de fluxos, é útil entender como é possível criar e aplicar funções de segunda ordem - funções para as quais outras funções servem como argumentos.
Então a beleza e a elegância dos fluxos serão totalmente reveladas a você.
Os fluxos são contagiosos. Depois de compreender a beleza deles, quero usá-los em todas as tarefas, o que obviamente não é necessário.
Em que tarefas é aconselhável usar fluxos e onde os métodos tradicionais devem ser usados, todos decidem por si mesmos.

Resumir


Neste artigo, tentei falar sobre os Modelos Mentais de Programação Reativa (MM RP) e até mesmo colocá-los parcialmente em sua Consciência. Vamos repetir os pontos principais novamente.

  1. Os MM RP são especiais, não semelhantes aos modelos mentais da programação tradicional.
  2. Ao iniciar a Programação Reativa, devemos lembrar que alguns bem estabelecidos em outras áreas do MM, como o cursor, cadeias de chamadas ou loops, não funcionam, ou não funcionam assim.
  3. O principal modelo mental de RP é um "fluxo de três canais" com um canal para elementos "normais", erros e informações sobre o final do fluxo.
  4. Os fluxos podem ser finitos e infinitos.
  5. «», «» «» . «» «».
  6. . (, ). .
  7. , .
  8. , .
  9. . .
  10. , «».


Se você está interessado neste tópico, pode "brincar" com fluxos usando os simuladores disponíveis no site [8].
Se você quiser entender melhor os conceitos de RP, recomendo que você trabalhe no livro [7] e, é claro, se familiarize com o Manifesto Reativo [11].
Você alcançará o próximo nível na formação de seu próprio MM RP trabalhando nos livros [9] e [10] sobre o design e modelagem de sistemas reativos.

Literatura e referências


  1. Programação é a materialização de idéias. (Artigo sobre Habr. Habr.com/ru/post/425321 )
  2. Sirotin V. RPSE: Reificação como Paradigma de Engenharia de Software. arxiv.org/abs/1810.01904
  3. Programação orientada a eventos. en.m.wikipedia.org/wiki/Event-driven_programming
  4. Dataflow-programming. en.m.wikipedia.org/wiki/Dataflow_programming
  5. Stream-processing. en.m.wikipedia.org/wiki/Stream_processing
  6. Rx-Extensions: reactivex.io/languages.html
  7. RxJS in Action. – 4. August 2017. Paul P. Daniels (Autor), Luis Atencio. Manning Publications. ISBN-13: 978-1617293412
  8. RxJS online Documentstion. xgrommx.imtqy.com/rx-book/index.html
  9. Reactive Design Patterns. 2017. Roland Kuhn Dr., Brian Hanafee, Jamie Allen. Manning Publications. ISBN-13: 978-1617291807
  10. Functional and Reactive Domain Modeling. 2016. Debasish Ghosh.Manning Publications. ISBN-13: 978-1617292248
  11. The Reactive Manifesto www.reactivemanifesto.org


: geralt

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


All Articles