La storia dimenticata di OOP

Nota: Questo fa parte della serie “Composing Software” (ora un libro!) sull’apprendimento della programmazione funzionale e delle tecniche software compositive in JavaScript ES6+ da zero. Restate sintonizzati. Ci sono molte altre cose che verranno!
Acquista il libro | Indice | < Precedente | Successivo >

I paradigmi di programmazione funzionale e imperativa che usiamo oggi sono stati esplorati matematicamente negli anni ’30 con il calcolo lambda e la macchina di Turing, che sono formulazioni alternative della computazione universale (sistemi formalizzati che possono eseguire una computazione generale). La tesi di Church Turing ha dimostrato che il calcolo lambda e le macchine di Turing sono funzionalmente equivalenti – che qualsiasi cosa che può essere calcolata usando una macchina di Turing può essere calcolata usando il calcolo lambda, e viceversa.

Nota: C’è un malinteso comune che le macchine di Turing possano calcolare qualsiasi cosa calcolabile. Ci sono classi di problemi (per esempio, il problema di halting) che possono essere calcolabili per alcuni casi, ma non sono generalmente calcolabili per tutti i casi usando le macchine di Turing. Quando uso la parola “computabile” in questo testo, intendo “computabile da una macchina di Turing”.

Lambda calculus rappresenta un approccio alla computazione di tipo top-down, con applicazione di funzioni, mentre la formulazione della macchina di Turing rappresenta un approccio alla computazione di tipo bottom-up, imperativo (step-by-step).

Linguaggi di basso livello come il codice macchina e l’assembly apparvero negli anni ’40, e alla fine degli anni ’50 apparvero i primi linguaggi popolari di alto livello. I dialetti Lisp sono ancora oggi di uso comune, inclusi Clojure, Scheme, AutoLISP, ecc. FORTRAN e COBOL apparvero entrambi negli anni ’50 e sono esempi di linguaggi imperativi di alto livello ancora in uso oggi, anche se i linguaggi della famiglia C hanno sostituito sia il COBOL che il FORTRAN per la maggior parte delle applicazioni.

Sia la programmazione imperativa che quella funzionale hanno le loro radici nella matematica della teoria del calcolo, prima dei computer digitali. “Object-Oriented Programming” (OOP) è stato coniato da Alan Kay nel 1966 o 1967 circa, mentre era alla scuola di specializzazione.

La seminale applicazione Sketchpad di Ivan Sutherland è stata una delle prime ispirazioni per la OOP. Fu creata tra il 1961 e il 1962 e pubblicata nella sua Sketchpad Thesis nel 1963. Gli oggetti erano strutture di dati che rappresentavano immagini grafiche visualizzate sullo schermo di un oscilloscopio, e presentavano un’ereditarietà tramite delegati dinamici, che Ivan Sutherland chiamava “master” nella sua tesi. Qualsiasi oggetto poteva diventare un “master”, e le istanze aggiuntive degli oggetti erano chiamate “occorrenze”. I master di Sketchpad hanno molto in comune con l’eredità prototipale di JavaScript.

Nota: Il TX-2 al MIT Lincoln Laboratory fu uno dei primi usi di un monitor grafico per computer che impiegava l’interazione diretta con lo schermo usando una penna leggera. L’EDSAC, che era operativo tra il 1948-1958 poteva visualizzare la grafica su uno schermo. Il Whirlwind al MIT aveva un display oscilloscopio funzionante nel 1949. La motivazione del progetto era quella di creare un simulatore di volo generale in grado di simulare il feedback degli strumenti per più aerei. Questo portò allo sviluppo del sistema di calcolo SAGE. Il TX-2 era un computer di prova per SAGE.

Il primo linguaggio di programmazione ampiamente riconosciuto come “orientato agli oggetti” fu Simula, specificato nel 1965. Come Sketchpad, Simula presentava oggetti, e alla fine introdusse classi, ereditarietà delle classi, sottoclassi e metodi virtuali.

Nota: Un metodo virtuale è un metodo definito su una classe che è progettato per essere sovrascritto dalle sottoclassi. I metodi virtuali permettono ad un programma di chiamare metodi che potrebbero non esistere al momento della compilazione del codice, impiegando il dispatch dinamico per determinare quale metodo concreto invocare in fase di esecuzione. JavaScript dispone di tipi dinamici e usa la catena di delega per determinare quali metodi invocare, quindi non ha bisogno di esporre il concetto di metodi virtuali ai programmatori. Detto altrimenti, tutti i metodi in JavaScript usano il dispatch dei metodi a runtime, quindi i metodi in JavaScript non hanno bisogno di essere dichiarati “virtuali” per supportare questa caratteristica.

“Ho inventato il termine ‘object-oriented’, e posso dirvi che non avevo in mente il C++”. ~ Alan Kay, OOPSLA ’97

Alan Kay ha coniato il termine “programmazione orientata agli oggetti” all’università nel 1966 o 1967. La grande idea era quella di usare mini-computer incapsulati nel software che comunicavano attraverso il passaggio di messaggi piuttosto che la condivisione diretta dei dati – per smettere di suddividere i programmi in “strutture dati” e “procedure” separate.

“Il principio di base del design ricorsivo è quello di fare in modo che le parti abbiano la stessa potenza del tutto”. ~ Bob Barton, il principale progettista del B5000, un mainframe ottimizzato per eseguire l’Algol-60.

Smalltalk fu sviluppato da Alan Kay, Dan Ingalls, Adele Goldberg e altri allo Xerox PARC. Smalltalk era più orientato agli oggetti di Simula – tutto in Smalltalk è un oggetto, incluse le classi, gli interi e i blocchi (chiusure). L’originale Smalltalk-72 non prevedeva la subclassificazione. Questa fu introdotta in Smalltalk-76 da Dan Ingalls.

Mentre Smalltalk supportava le classi ed eventualmente le sottoclassi, Smalltalk non riguardava le classi o le sottoclassi. Era un linguaggio funzionale ispirato dal Lisp e da Simula. Alan Kay ritiene che l’attenzione dell’industria sulle sottoclassi sia una distrazione dai veri benefici della programmazione orientata agli oggetti.

“Mi dispiace di aver coniato molto tempo fa il termine “oggetti” per questo argomento perché fa sì che molte persone si concentrino sull’idea minore. La grande idea è la messaggistica.”
~ Alan Kay

In uno scambio di email del 2003, Alan Kay chiarì cosa intendeva quando chiamava Smalltalk “object-oriented”:

“OOP per me significa solo messaggistica, conservazione locale e protezione e nascondimento dello stato-processo, ed estremo late-binding di ogni cosa.”
~ Alan Kay

In altre parole, secondo Alan Kay, gli ingredienti essenziali della OOP sono:

  • Message passing
  • Encapsulation
  • Dynamic binding

In particolare, l’ereditarietà e il polimorfismo delle sottoclassi NON erano considerati ingredienti essenziali della OOP da Alan Kay, l’uomo che ha coniato il termine e portato la OOP alle masse.

L’essenza dell’OOP

La combinazione di message passing e incapsulamento servono alcuni scopi importanti:

  • Evitare lo stato mutevole condiviso incapsulando lo stato e isolando altri oggetti dai cambiamenti di stato locali. L’unico modo per influenzare lo stato di un altro oggetto è chiedere (non comandare) a quell’oggetto di cambiarlo inviando un messaggio. I cambiamenti di stato sono controllati ad un livello locale e cellulare piuttosto che esposti ad un accesso condiviso.
  • Disaccoppiamento degli oggetti l’uno dall’altro – il mittente del messaggio è solo debolmente accoppiato al ricevitore del messaggio, attraverso l’API di messaggistica.
  • Adattabilità e resilienza ai cambiamenti in fase di esecuzione tramite late binding. L’adattabilità runtime fornisce molti grandi benefici che Alan Kay considerava essenziali per l’OOP.

Queste idee sono state ispirate dalle cellule biologiche e/o dai singoli computer su una rete attraverso il background di Alan Kay in biologia e l’influenza della progettazione di Arpanet (una prima versione di Internet). Anche così presto, Alan Kay immaginava un software in esecuzione su un gigantesco computer distribuito (Internet), dove i singoli computer agivano come cellule biologiche, operando indipendentemente sul proprio stato isolato, e comunicando attraverso il passaggio di messaggi.

“Mi resi conto che la metafora della cella/intero computer avrebbe eliminato i dati”
~ Alan Kay

Con “eliminare i dati”, Alan Kay era sicuramente consapevole dei problemi dello stato mutevole condiviso e dello stretto accoppiamento causato dai dati condivisi – temi comuni oggi.

Ma alla fine degli anni ’60, i programmatori dell’ARPA erano frustrati dalla necessità di scegliere una rappresentazione del modello di dati per i loro programmi prima di costruire il software. Le procedure che erano troppo strettamente accoppiate a particolari strutture di dati non erano resistenti ai cambiamenti. Volevano un trattamento più omogeneo dei dati.

” l’intero punto dell’OOP è di non doversi preoccupare di ciò che è dentro un oggetto. Gli oggetti fatti su macchine diverse e con linguaggi diversi dovrebbero essere in grado di parlare tra loro” ~ Alan Kay

Gli oggetti possono astrarre e nascondere le implementazioni delle strutture dati. L’implementazione interna di un oggetto può cambiare senza rompere altre parti del sistema software. Infatti, con un estremo binding tardivo, un sistema informatico completamente diverso potrebbe assumere le responsabilità di un oggetto, e il software potrebbe continuare a funzionare. Gli oggetti, nel frattempo, potrebbero esporre un’interfaccia standard che funziona con qualsiasi struttura dati che l’oggetto usa internamente. La stessa interfaccia potrebbe funzionare con una lista collegata, un albero, un flusso, e così via.

Alan Kay vedeva anche gli oggetti come strutture algebriche, che danno certe garanzie matematicamente dimostrabili sui loro comportamenti:

“Il mio background matematico mi fece capire che ogni oggetto poteva avere diverse algebre associate ad esso, e ci potevano essere famiglie di queste, e che queste sarebbero state molto molto utili.”
~ Alan Kay

Questo si è dimostrato vero, e costituisce la base per oggetti come le promesse e le lenti, entrambi ispirati alla teoria delle categorie.

La natura algebrica della visione di Alan Kay per gli oggetti permetterebbe agli oggetti di permettere verifiche formali, comportamento deterministico e migliore testabilità, perché le algebre sono essenzialmente operazioni che obbediscono a poche regole sotto forma di equazioni.

Nel gergo dei programmatori, le algebre sono come astrazioni fatte di funzioni (operazioni) accompagnate da leggi specifiche applicate da test unitari che quelle funzioni devono superare (assiomi/equazioni).

Queste idee sono state dimenticate per decenni nella maggior parte dei linguaggi OO della famiglia C, inclusi C++, Java, C#, ecc, ma stanno cominciando a ritrovare la loro strada nelle versioni recenti dei linguaggi OO più usati.

Si potrebbe dire che il mondo della programmazione sta riscoprendo i benefici della programmazione funzionale e del pensiero ragionato nel contesto dei linguaggi OO.

Come JavaScript e Smalltalk prima di esso, la maggior parte dei moderni linguaggi OO stanno diventando sempre più “linguaggi multi-paradigma”. Non c’è motivo di scegliere tra programmazione funzionale e OOP. Quando guardiamo all’essenza storica di ciascuno, non sono solo compatibili, ma idee complementari.

Perché condividono così tante caratteristiche in comune, mi piace dire che JavaScript è la vendetta di Smalltalk sull’incomprensione mondiale dell’OOP. Both Smalltalk and JavaScript support:

  • Objects
  • First-class functions and closures
  • Dynamic types
  • Late binding (functions/methods changeable at runtime)
  • OOP without class inheritance

What is essential to OOP (according to Alan Kay)?

  • Encapsulation
  • Message passing
  • Dynamic binding (the ability for the program to evolve/adapt at runtime)

What is non-essential?

  • Classes
  • Class inheritance
  • Special treatment for objects/functions/data
  • The new keyword
  • Polymorphism
  • Static types
  • Recognizing a class as a “type”

If your background is Java or C#, you may be thinking static types and Polymorphism are essential ingredients, but Alan Kay preferred dealing with generic behaviors in algebraic form. For example, from Haskell:

fmap :: (a -> b) -> f a -> f b

This is the functor map signature, which acts generically over unspecified types a and b, applying a function from a to b in the context of a functor of a to produce a functor of b. Functor is math jargon that essentially means “supporting the map operation”. If you’re familiar with .map() in JavaScript, you already know what that means.

Here are two examples in JavaScript:

// isEven = Number => Boolean
const isEven = n => n % 2 === 0;const nums = ;// map takes a function `a => b` and an array of `a`s (via `this`)
// and returns an array of `b`s.
// in this case, `a` is `Number` and `b` is `Boolean`
const results = nums.map(isEven);console.log(results);
//

The .map() method is generic in the sense that a and b can be any type, and .map() handles it just fine because arrays are data structures that implement the algebraic functor laws. I tipi non hanno importanza per .map() perché non cerca di manipolarli direttamente, applicando invece una funzione che si aspetta e restituisce i tipi corretti per l’applicazione.

// matches = a => Boolean
// here, `a` can be any comparable type
const matches = control => input => input === control;const strings = ;const results = strings.map(matches('bar'));console.log(results);
//

Questa relazione generica tra tipi è difficile da esprimere correttamente e completamente in un linguaggio come TypeScript, ma era abbastanza facile da esprimere nei tipi Hindley Milner di Haskell con supporto per tipi di tipo superiore (tipi di tipi).

La maggior parte dei sistemi di tipi sono stati troppo restrittivi per permettere la libera espressione di idee dinamiche e funzionali, come la composizione di funzioni, la composizione libera di oggetti, l’estensione di oggetti runtime, i combinatori, gli obiettivi, ecc. In altre parole, i tipi statici rendono spesso più difficile scrivere software componibile.

Se il vostro sistema di tipi è troppo restrittivo (ad esempio, TypeScript, Java), siete costretti a scrivere codice più contorto per raggiungere gli stessi obiettivi. Questo non significa che i tipi statici siano una cattiva idea, o che tutte le implementazioni dei tipi statici siano ugualmente restrittive. Ho incontrato molti meno problemi con il sistema di tipi di Haskell.

Se siete un fan dei tipi statici e non vi dispiacciono le restrizioni, più potere a voi, ma se trovate alcuni dei consigli in questo testo difficili perché è difficile scrivere funzioni composte e strutture algebriche composte, incolpate il sistema di tipi, non le idee. La gente ama il comfort dei loro SUV, ma nessuno si lamenta che non ti permettono di volare. Per quello, avete bisogno di un veicolo con più gradi di libertà.

Se le restrizioni rendono il vostro codice più semplice, bene! Ma se le restrizioni ti costringono a scrivere codice più complicato, forse le restrizioni sono sbagliate.

Cos’è un oggetto?

Gli oggetti hanno chiaramente assunto molte connotazioni nel corso degli anni. Ciò che chiamiamo “oggetti” in JavaScript sono semplicemente tipi di dati compositi, senza nessuna delle implicazioni della programmazione basata su classi o del message-passing di Alan Kay.

In JavaScript, questi oggetti possono supportare, e spesso lo fanno, l’incapsulamento, il passaggio di messaggi, la condivisione del comportamento attraverso i metodi, persino il polimorfismo delle sottoclassi (anche se usando una catena di delega piuttosto che un dispatch basato sui tipi). Si può assegnare qualsiasi funzione a qualsiasi proprietà. Potete costruire i comportamenti degli oggetti dinamicamente, e cambiare il significato di un oggetto in fase di esecuzione. JavaScript supporta anche l’incapsulamento usando le chiusure per la privacy dell’implementazione. Ma tutto questo è un comportamento opt-in.

La nostra idea attuale di un oggetto è semplicemente una struttura dati composita, e non richiede niente di più per essere considerato un oggetto. Ma programmare usando questi tipi di oggetti non rende il vostro codice “orientato agli oggetti” più di quanto programmare con le funzioni renda il vostro codice “funzionale”.

OOP non è più OOP reale

Perché “oggetto” nei moderni linguaggi di programmazione significa molto meno di quanto significasse per Alan Kay, sto usando “componente” invece di “oggetto” per descrivere le regole della vera OOP. Molti oggetti sono posseduti e manipolati direttamente da altro codice in JavaScript, ma i componenti dovrebbero incapsulare e controllare il proprio stato.

Real OOP significa:

  • Programmare con componenti (l'”oggetto” di Alan Kay)
  • Lo stato dei componenti deve essere incapsulato
  • Utilizzare il message passing per la comunicazione tra oggetti
  • I componenti possono essere aggiunti/modificati/sostituiti a runtime

I comportamenti della maggior parte dei componenti possono essere specificati genericamente usando strutture dati algebriche. L’ereditarietà non è necessaria qui. I componenti possono riutilizzare i comportamenti dalle funzioni condivise e dalle importazioni modulari senza condividere i loro dati.

Manipolare oggetti o usare l’ereditarietà delle classi in JavaScript non significa che si sta “facendo OOP”. Usare i componenti in questo modo sì. Ma l’uso popolare è il modo in cui le parole vengono definite, quindi forse dovremmo abbandonare l’OOP e chiamare questo “Message Oriented Programming (MOP)” invece di “Object Oriented Programming (OOP)”?

È una coincidenza che gli stracci siano usati per pulire i pasticci?

Come è fatta una buona MOP

Nel software più moderno, c’è un’interfaccia utente responsabile della gestione delle interazioni con l’utente, del codice che gestisce lo stato dell’applicazione (dati utente), e del codice che gestisce gli I/O di sistema o di rete.

Ognuno di questi sistemi può richiedere processi di lunga durata, come gli ascoltatori di eventi, lo stato per tenere traccia di cose come la connessione di rete, lo stato degli elementi dell’interfaccia utente e lo stato dell’applicazione stessa.

Una buona MOP significa che invece di tutti questi sistemi che raggiungono e manipolano direttamente lo stato dell’altro, il sistema comunica con gli altri componenti attraverso la distribuzione dei messaggi. Quando l’utente fa clic su un pulsante di salvataggio, un messaggio "SAVE" potrebbe essere distribuito, che un componente dello stato dell’applicazione potrebbe interpretare e trasmettere a un gestore di aggiornamento dello stato (come una funzione di riduttore puro). Forse dopo che lo stato è stato aggiornato, il componente di stato potrebbe inviare un messaggio "STATE_UPDATED" a un componente UI, che a sua volta interpreterà lo stato, riconcilierà quali parti dell’UI hanno bisogno di essere aggiornate, e trasmetterà lo stato aggiornato ai sottocomponenti che gestiscono quelle parti dell’UI.

Nel frattempo, il componente della connessione di rete potrebbe monitorare la connessione dell’utente a un’altra macchina sulla rete, ascoltare i messaggi e inviare rappresentazioni aggiornate dello stato per salvare i dati su una macchina remota. Sta internamente tenendo traccia di un timer di heartbeat della rete, se la connessione è attualmente online o offline, e così via.

Questi sistemi non hanno bisogno di conoscere i dettagli delle altre parti del sistema. Solo delle loro preoccupazioni individuali e modulari. I componenti del sistema sono scomponibili e ricomponibili. Implementano interfacce standardizzate in modo che siano in grado di interoperare. Finché l’interfaccia è soddisfatta, si possono sostituire i componenti che possono fare la stessa cosa in modi diversi, o cose completamente diverse con gli stessi messaggi. Potreste anche farlo a runtime, e tutto continuerebbe a funzionare correttamente.

I componenti dello stesso sistema software potrebbero anche non aver bisogno di trovarsi sulla stessa macchina. Il sistema potrebbe essere decentralizzato. Lo storage di rete potrebbe suddividere i dati su un sistema di storage decentralizzato come IPFS, in modo che l’utente non dipenda dalla salute di una particolare macchina per assicurarsi che i suoi dati siano al sicuro, e al sicuro dagli hacker che potrebbero volerli rubare.

OOP è stato parzialmente ispirato da Arpanet, e uno degli obiettivi di Arpanet era di costruire una rete decentralizzata che potesse essere resistente ad attacchi come bombe atomiche. Secondo il direttore della DARPA durante lo sviluppo di Arpanet, Stephen J. Lukasik (“Why the Arpanet Was Built”):

“L’obiettivo era quello di sfruttare le nuove tecnologie informatiche per soddisfare le esigenze di comando e controllo militare contro le minacce nucleari, ottenere un controllo sopravvivibile delle forze nucleari statunitensi, e migliorare il processo decisionale militare tattico e gestionale.”

Nota: L’impulso primario di Arpanet era la convenienza piuttosto che la minaccia nucleare, e i suoi ovvi vantaggi di difesa sono emersi più tardi. ARPA stava usando tre terminali di computer separati per comunicare con tre progetti di ricerca informatica separati. Bob Taylor voleva una singola rete di computer per connettere ogni progetto con gli altri.

Un buon sistema MOP potrebbe condividere la robustezza di internet usando componenti che sono hot-swappable mentre l’applicazione è in esecuzione. Potrebbe continuare a funzionare se l’utente è su un telefono cellulare e va offline perché è entrato in un tunnel. Potrebbe continuare a funzionare se un uragano mettesse fuori uso la corrente in uno dei data center dove si trovano i server.

È ora che il mondo del software lasci andare l’esperimento fallito dell’ereditarietà di classe, e abbracci i principi matematici e scientifici che originariamente definivano lo spirito dell’OOP.

È ora di iniziare a costruire software più flessibile, più resiliente, meglio composto, con la MOP e la programmazione funzionale che lavorano in armonia.

Nota: L’acronimo MOP è già usato per descrivere la “programmazione orientata al monitoraggio” ed è improbabile che l’OOP se ne vada tranquillamente.
Fate la MOP sulle vostre OOP.

Impara di più su EricElliottJS.com

Le lezioni video sulla programmazione funzionale sono disponibili per i membri di EricElliottJS.com. Se non sei un membro, iscriviti oggi stesso.