Monkey Patching em linguagens de programação dinâmicas: um exemplo de JavaScript

Javascript
Programação Dinâmica
Monkey Patching em linguagens de programação dinâmicas cover image

Introdução

Este artigo explorará os conceitos de linguagens de programação dinâmica e estática, as principais diferenças entre as duas e o que cada paradigma oferece em termos de vantagens e armadilhas. Esta exploração se concentrará ainda mais em linguagens de programação dinâmicas, particularmente em um dos padrões essenciais que ela permite: Monkey Patch. Esse padrão será apresentado com a ajuda de um exemplo em JavaScript.

Linguagens de programação dinâmicas versus estáticas

Terminologia

Para entender o que constitui uma linguagem dinâmica ou estática, precisamos estabelecer uma compreensão de alguns termos-chave comumente usados ​​neste contexto: Tempo de compilação, Tempo de execução e *Verificação de tipo *.

Compilar e Tempo de Execução são dois termos que correspondem a diferentes estágios do ciclo de vida de um programa de computador, começando com Tempo de compilação.

Tempo de compilação

O tempo de compilação é a primeira etapa do ciclo de vida de um programa. Um desenvolvedor escreve código em uma determinada linguagem de programação. Na maioria das vezes, a máquina não consegue entender o código escrito em uma linguagem de alto nível, então um compilador dedicado é usado para traduzi-lo para um formato intermediário de nível inferior que fica pronto para execução.

Tempo de execução

O tempo de execução geralmente encapsula duas etapas: carregar o programa na memória, alocando os recursos necessários para sua execução junto com suas instruções, e então executar o programa seguindo a ordem dessas instruções.

O diagrama a seguir ilustra esse processo:

Verificação de tipo

A verificação de tipo é um recurso integrado em quase todas as linguagens de programação. É a capacidade de verificar se um valor atribuído a uma determinada variável corresponde ao tipo correto dessa variável. Cada linguagem de programação possui uma maneira diferente de representar um valor de um determinado tipo na memória. Estas diferentes representações permitem verificar a correspondência entre o tipo de valor e o tipo de variável à qual você tenta atribuir esse valor.

Agora que temos um entendimento de alto nível do ciclo de vida de um programa e da verificação de tipo, podemos prosseguir para explorar linguagens de programação estáticas.

Linguagens de programação estática

Linguagens de programação estáticas, também chamadas de linguagens de tipo estaticamente, são linguagens que aplicam a verificação de tipo que mencionamos na fase de compilação. Isso efetivamente significa que uma variável mantém seu tipo na declaração e nenhum valor pode ser atribuído a ela além dos valores de seu tipo de declaração. Linguagens de programação estáticas oferecem segurança extra ao lidar com tipos, mas podem retardar o processo de desenvolvimento em certos casos de uso quando isso se torna uma restrição severa.

Linguagens de programação dinâmicas

As linguagens de programação dinâmica, por outro lado, aplicam verificação de tipo em tempo de execução. Isso significa que qualquer variável pode conter qualquer valor em qualquer ponto do programa. Isso pode ser benéfico, pois oferece ao desenvolvedor um nível de flexibilidade que não está presente nas linguagens estáticas. Linguagens dinâmicas tendem a ser mais lentas na execução do que suas contrapartes estáticas, pois envolvem uma etapa adicional de descobrir dinamicamente a digitação de cada variável.

Macaco Patch

A digitação estática versus dinâmica é uma característica fundamental em uma linguagem de programação; optar por um paradigma em vez de outro pode permitir uma série de padrões e práticas diferentes que podem melhorar significativamente a qualidade e a velocidade de desenvolvimento. Ele também pode abrir a porta para muitas limitações e antipadrões se nenhuma consideração cuidadosa for feita ao tomar decisões de design.

Particularmente, sabe-se que linguagens de programação de tipo dinâmico oferecem um nível mais alto de flexibilidade, uma vez que não restringem uma variável a um único tipo. Essa flexibilidade acarreta o custo de responsabilidade adicional para o desenvolvedor ao implementar e depurar programas para garantir que nenhum comportamento imprevisível ocorra. O padrão de patch de macaco vem dessa filosofia.

Monkey Patch refere-se ao processo de estender/alterar o funcionamento de um componente em tempo de execução. O componente em questão pode ser uma biblioteca, uma classe, um método ou até mesmo um módulo. A ideia é a mesma: um trecho de código é feito para realizar uma determinada tarefa, e o objetivo do monkey patching é alterar ou estender o comportamento desse trecho de código para que ele realize uma nova tarefa, tudo sem alterar o próprio código. .

Isso é possível na linguagem de programação dinâmica, pois não importa com que tipo de componente estamos lidando, ele ainda possui a mesma estrutura de um objeto com atributos diferentes, os atributos podem conter métodos que podem ser reatribuídos para alcançar um novo comportamento no objeto sem entrar em seus detalhes internos e de implementação. Isso se torna particularmente útil no caso de bibliotecas e módulos de terceiros, pois eles tendem a ser mais difíceis de ajustar.

O exemplo a seguir mostrará um caso de uso comum que pode se beneficiar do uso da técnica de patch de macaco. Javascript foi usado para fins de implementação aqui, mas ainda deve se aplicar amplamente a qualquer outra linguagem de programação dinâmica.

Exemplo

Implemente uma estrutura de teste mínimo com o módulo HTTP nativo do Node

Os testes de unidade e integração podem se enquadrar nos casos de uso de patches do Monkey. Eles geralmente envolvem casos de teste que abrangem mais de um serviço para testes de integração ou dependências de API e/ou banco de dados para testes unitários. Nestes dois cenários, e para cumprir os objetivos dos testes, em primeiro lugar, gostaríamos que os nossos testes fossem independentes destes recursos externos. A maneira de conseguir isso é zombando. Zombar é simular o comportamento de serviços externos para que o teste possa se concentrar na lógica real do código. O Monkey Patching pode ser útil aqui, pois pode modificar os métodos dos serviços externos, substituindo-os por métodos de espaço reservado que chamamos de “stub”. Esses métodos retornam o resultado esperado nos casos de teste para que possamos evitar o início de solicitações aos serviços de produção apenas para fins de testes.

O exemplo a seguir é uma implementação simples de patch do Monkey no módulo http nativo do NodeJs. O módulo http é a interface que implementa os métodos do protocolo http para NodeJs. É usado principalmente para criar servidores http barebone e comunicar-se com serviços externos usando o protocolo http.

No exemplo abaixo temos um caso de teste simples onde chamamos um serviço externo para buscar a lista de IDs de usuários. Em vez de chamar o serviço real, corrigimos o método http get para que ele apenas retorne o resultado esperado, que é uma matriz de IDs de usuários aleatórios. Isso pode não parecer de grande importância, já que estamos apenas buscando dados, mas se implementarmos outro caso de teste que envolva algum tipo de alteração de dados, poderemos alterar acidentalmente os dados na produção ao executar os testes.

Desta forma podemos implementar as nossas funcionalidades e escrever testes para cada funcionalidade garantindo ao mesmo tempo a segurança dos nossos serviços de produção.

// import the http module
let http = require("http");

// patch the get method of the http module
http.get = async function(url) {
  return {
    data: ["1234", "1235", "1236", "1236"]
  };
}

// example test suite, call new patched get method for testing
test('get array of user ids from users api', async () => {
  const res = await http.get("https://users.api.com/ids");
  const userIds = res.data;
  expect(userIds).toBeDefined();
  expect(userIds.length).toBe(4);
  expect(userIds[0]).toBe("1234");
});

O código acima é simples, importamos o módulo http, reatribuímos o método http.get com um novo método que apenas retorna um array de ids. Agora chamamos o novo método corrigido dentro do caso de teste e obtemos o novo resultado esperado.

~/SphericalTartWorker$ npm test

> nodejs@1.0.0 test
> jest

PASS  ./index.test.js
  ✓ get array of user ids from users api (25 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.977 s, estimated 2 s
Ran all test suites.

Armadilhas e Limitações Comuns

Não deveria ser surpresa que o monkey patching tenha suas próprias falhas e limitações. No contexto de módulos no sistema de módulos de nós, corrigir um módulo global como http é considerado uma operação com efeitos colaterais, isso ocorre porque http é acessível de qualquer ponto dentro da base de código e qualquer outra entidade pode depender dele. Essas entidades esperam que o módulo http opere em seu comportamento normal. Ao alterar um dos métodos http, efetivamente quebramos todas as outras dependências http dentro da base de código.

Como estamos operando em uma linguagem de tipo dinâmico, as coisas podem não falhar imediatamente e prefeririam assumir um comportamento imprevisível que torna a depuração uma tarefa extremamente complexa. Em outros casos de uso, pode haver dois patches diferentes do mesmo componente no mesmo atributo; nesse caso, não podemos realmente prever qual patch terá precedência sobre o outro, resultando em um código ainda mais imprevisível.

Também é importante mencionar que o monkey patching pode ter pequenas variações de comportamento entre diferentes linguagens de programação. Tudo depende do design da linguagem e das escolhas de implementação. Por exemplo, em python, nem todas as instâncias que usam um método corrigido serão afetadas pelo patch. Se uma instância chamar explicitamente o método corrigido, ela obterá a nova versão atualizada, pelo contrário, outras instâncias que possam ter apenas atributos apontando para o método corrigido e não o chamando explicitamente obterão a versão original, isso se deve à forma como o python a vinculação nas classes opera.

Conclusão

Neste artigo, exploramos as distinções de alto nível entre linguagens de programação estáticas e dinâmicas e vimos como as linguagens de programação dinâmicas podem se beneficiar de novos paradigmas e padrões, aproveitando a flexibilidade inerente que essas linguagens oferecem. O exemplo que apresentamos estava relacionado ao Monkey patching, uma técnica usada para estender o comportamento do código sem alterá-lo na fonte. Vimos um caso em que o uso desta técnica seria benéfico juntamente com as suas potenciais desvantagens. O desenvolvimento de software envolve compensações, e empregar a solução certa para o problema requer considerações elaboradas do desenvolvedor e um bom entendimento dos princípios e fundamentos da arquitetura.


Career Services background pattern

Serviços de carreira

Contact Section background image

Vamos manter-nos em contacto

Code Labs Academy © 2024 Todos os direitos reservados.