Monkey Patching nei linguaggi di programmazione dinamici: un esempio JavaScript

Javascript
programmazione dinamica
Monkey Patching nei linguaggi di programmazione dinamici cover image

Introduzione

Questo articolo esplorerà i concetti dei linguaggi di programmazione dinamici e statici, le principali differenze tra i due e ciò che ciascun paradigma offre in termini di vantaggi e insidie. Questa esplorazione si concentrerà ulteriormente sui linguaggi di programmazione dinamici, in particolare su uno dei pattern essenziali che abilita: Monkey Patch, questo pattern verrà presentato con l'aiuto di un esempio in JavaScript.

Linguaggi di programmazione dinamici e statici

Terminologia

Per capire cosa costituisce un linguaggio dinamico o statico, dobbiamo comprendere alcuni termini chiave comunemente usati in questo contesto: Tempo di compilazione, Runtime e *Controllo del tipo *.

Compilazione e Runtime sono due termini che corrispondono a diverse fasi del ciclo di vita di un programma per computer, a partire dal tempo di compilazione.

Ora di compilazione

Il tempo di compilazione è il primo passo nel ciclo di vita di un programma. Uno sviluppatore scrive il codice in un determinato linguaggio di programmazione. Nella maggior parte dei casi, la macchina non è in grado di comprendere il codice scritto in un linguaggio di alto livello, quindi viene utilizzato un compilatore dedicato per tradurlo in un formato intermedio di livello inferiore che diventa pronto per l'esecuzione.

Tempo di esecuzione

Il runtime di solito incapsula due passaggi: caricare il programma in memoria allocando le risorse necessarie per la sua esecuzione insieme alle sue istruzioni, quindi eseguire il programma seguendo l'ordine di tali istruzioni.

Il diagramma seguente illustra questo processo:

Controllo del tipo

Il controllo del tipo è una funzionalità integrata in quasi tutti i linguaggi di programmazione. È la capacità di verificare se un valore assegnato a una determinata variabile corrisponde al tipo corretto di quella variabile. Ogni linguaggio di programmazione ha un modo diverso di rappresentare un valore di un dato tipo in memoria. Queste diverse rappresentazioni permettono di verificare la corrispondenza tra il tipo di un valore e il tipo di variabile a cui si tenta di assegnare quel valore.

Ora che abbiamo una conoscenza di alto livello del ciclo di vita di un programma e del controllo del tipo, possiamo procedere con l'esplorazione dei linguaggi di programmazione statici.

Linguaggi di programmazione statici

I linguaggi di programmazione statici, detti anche linguaggi tipizzati staticamente, sono linguaggi che applicano il controllo del tipo di cui abbiamo parlato in fase di compilazione. Ciò significa effettivamente che una variabile mantiene il suo tipo dalla dichiarazione e non può essere assegnato ad essa alcun valore diverso dai valori del suo tipo di dichiarazione. I linguaggi di programmazione statici offrono maggiore sicurezza quando si ha a che fare con i tipi, ma possono rallentare il processo di sviluppo in alcuni casi d'uso quando ciò diventa una dura restrizione.

Linguaggi di programmazione dinamici

I linguaggi di programmazione dinamici, d'altra parte, applicano il controllo del tipo in fase di esecuzione. Ciò significa che qualsiasi variabile può contenere qualsiasi valore in qualsiasi punto del programma. Ciò può essere vantaggioso in quanto offre allo sviluppatore un livello di flessibilità che non è presente nei linguaggi statici. I linguaggi dinamici tendono ad essere più lenti in fase di esecuzione rispetto alle loro controparti statiche poiché implicano un ulteriore passaggio per determinare dinamicamente la tipizzazione di ciascuna variabile.

Patch della scimmia

La digitazione statica e dinamica è un tratto fondamentale in un linguaggio di programmazione, l'utilizzo di un paradigma rispetto all'altro può abilitare una serie di modelli e pratiche diversi che possono migliorare significativamente la qualità e la velocità di sviluppo. Può anche aprire la porta a molte limitazioni e anti-modelli se non vengono fatte attente considerazioni quando si prendono decisioni di progettazione.

In particolare, è noto che i linguaggi di programmazione tipizzati dinamicamente offrono un livello più elevato di flessibilità poiché non limitano una variabile a un singolo tipo. Questa flessibilità comporta il costo di una responsabilità aggiuntiva per lo sviluppatore durante l'implementazione e il debug dei programmi per assicurarsi che non si verifichino comportamenti imprevedibili. Il motivo della patch della scimmia deriva da questa filosofia.

Monkey Patch si riferisce al processo di estensione/modifica del funzionamento di un componente in fase di runtime. Il componente in questione può essere una libreria, una classe, un metodo o anche un modulo. L'idea è la stessa: un pezzo di codice viene creato per svolgere un determinato compito e l'obiettivo del Monkey Patching è modificare o estendere il comportamento di quel pezzo di codice in modo che possa svolgere un nuovo compito, il tutto senza modificare il codice stesso. .

Ciò è reso possibile nel linguaggio di programmazione dinamico poiché non importa con quale tipo di componente abbiamo a che fare, ha sempre la stessa struttura di un oggetto con attributi diversi, gli attributi possono contenere metodi che possono essere riassegnati per ottenere un nuovo comportamento nell'oggetto senza entrare nei suoi aspetti interni e nei dettagli di attuazione. Ciò diventa particolarmente utile in caso di librerie e moduli di terze parti poiché tendono ad essere più difficili da modificare.

L'esempio seguente mostrerà un caso d'uso comune che può trarre vantaggio dall'utilizzo della tecnica delle scimmie patch. Javascript è stato utilizzato per motivi di implementazione qui, ma questo dovrebbe comunque applicarsi ampiamente a qualsiasi altro linguaggio di programmazione dinamica.

Esempio

Implementa una struttura di test minima con il modulo HTTP nativo di Node

I test unitari e di integrazione possono rientrare nei casi d'uso delle patch Monkey. Di solito coinvolgono casi di test che si estendono su più di un servizio per i test di integrazione o dipendenze API e/o database per i test unitari. In questi due scenari, e per raggiungere in primo luogo gli obiettivi del test, vorremmo che i nostri test fossero indipendenti da queste risorse esterne. Il modo per raggiungere questo obiettivo è attraverso la derisione. Il mocking consiste nel simulare il comportamento di servizi esterni in modo che il test possa concentrarsi sulla logica effettiva del codice. Il Monkey Patching può essere utile in questo caso poiché può modificare i metodi dei servizi esterni sostituendoli con metodi segnaposto che chiamiamo “stub”. Questi metodi restituiscono il risultato atteso nei casi di test in modo da poter evitare di avviare richieste ai servizi di produzione solo per motivi di test.

L'esempio seguente è una semplice implementazione dell'applicazione di patch Monkey sul modulo http nativo di NodeJs. Il modulo http è l'interfaccia che implementa i metodi del protocollo http per NodeJs. Viene utilizzato principalmente per creare server http barebone e comunicare con servizi esterni utilizzando il protocollo http.

Nell'esempio seguente abbiamo un semplice caso di test in cui chiamiamo un servizio esterno per recuperare l'elenco degli ID utente. Invece di chiamare il servizio vero e proprio, applichiamo una patch al metodo http get in modo che restituisca semplicemente il risultato atteso che è un array di ID utente casuali. Questo potrebbe non sembrare di grande importanza poiché stiamo solo recuperando dati, ma se implementiamo un altro caso di test che comporta l'alterazione dei dati di qualche tipo, potremmo alterare accidentalmente i dati sulla produzione durante l'esecuzione dei test.

In questo modo possiamo implementare le nostre funzionalità e scrivere test per ciascuna funzionalità garantendo la sicurezza dei nostri servizi di produzione.

// 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");
});

Il codice sopra è semplice, importiamo il modulo http, riassegniamo il metodo http.get con un nuovo metodo che restituisce semplicemente un array di ID. Ora chiamiamo il nuovo metodo patchato all'interno del test case e otteniamo il nuovo risultato atteso.

~/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.

Insidie ​​e limitazioni comuni

Non dovrebbe sorprendere che il sistema di patching delle scimmie abbia i suoi difetti e limiti. Nel contesto dei moduli nel sistema dei moduli nodo, applicare patch a un modulo globale come http è considerata un'operazione con effetti collaterali, questo perché http è accessibile da qualsiasi punto all'interno della codebase e qualsiasi altra entità potrebbe avere una dipendenza da esso. Queste entità si aspettano che il modulo http funzioni nel suo comportamento abituale, modificando uno dei metodi http interrompiamo effettivamente tutte le altre dipendenze http all'interno della base di codice.

Dato che stiamo operando all'interno di un linguaggio tipizzato dinamicamente, le cose potrebbero non fallire immediatamente e preferirebbero assumere un comportamento imprevedibile che rende il debugging un compito estremamente complesso. In altri casi d'uso, potrebbero esserci due patch diverse dello stesso componente sullo stesso attributo, nel qual caso non possiamo realmente prevedere quale patch avrà la precedenza sull'altra, risultando in un codice ancora più imprevedibile.

È anche importante ricordare che l'applicazione delle patch alle scimmie potrebbe presentare lievi variazioni di comportamento tra i diversi linguaggi di programmazione. Tutto dipende dalla progettazione del linguaggio e dalle scelte di implementazione. Ad esempio, in Python, non tutte le istanze che utilizzano un metodo con patch saranno interessate dalla patch. Se un'istanza chiama esplicitamente il metodo patched allora otterrà la nuova versione aggiornata, al contrario, altre istanze che potrebbero avere solo attributi che puntano al metodo patched e non lo chiamano esplicitamente otterranno la versione originale, questo è dovuto a come python opera il legame nelle classi.

Conclusione

In questo articolo abbiamo esplorato le distinzioni di alto livello tra linguaggi di programmazione statici e dinamici, abbiamo visto come i linguaggi di programmazione dinamici possono trarre vantaggio da nuovi paradigmi e modelli sfruttando la flessibilità intrinseca offerta da questi linguaggi. L'esempio che abbiamo presentato riguardava il patching di Monkey, una tecnica utilizzata per estendere il comportamento del codice senza modificarlo dall'origine. Abbiamo visto un caso in cui l'uso di questa tecnica sarebbe vantaggioso insieme ai suoi potenziali svantaggi. Lo sviluppo del software è una questione di compromessi e l'impiego della soluzione giusta per il problema richiede considerazioni elaborate da parte dello sviluppatore e una buona comprensione dei principi e dei fondamenti dell'architettura.


Career Services background pattern

Servizi per le carriere

Contact Section background image

Rimaniamo in contatto

Code Labs Academy © 2024 Tutti i diritti riservati.