Arrivederci, programmazione orientata agli oggetti

Ho programmato in lingue orientate agli oggetti per decenni. Il primo linguaggio OO che ho usato era C ++ e poi Smalltalk e infine .NET e Java.

Sono stato entusiasta di sfruttare i vantaggi di ereditarietà, incapsulamento e polimorfismo. I tre pilastri del paradigma.

Ero ansioso di ottenere la promessa di Riuso e sfruttare la saggezza acquisita da coloro che mi hanno preceduto in questo nuovo ed eccitante panorama.

Non riuscivo a contenere la mia eccitazione al pensiero di mappare i miei oggetti del mondo reale nelle loro Classi e mi aspettavo che il mondo intero si sistemasse perfettamente.

Non avrei potuto essere più sbagliato.

Eredità, il primo pilastro a cadere

A prima vista, l'ereditarietà sembra essere il principale vantaggio del paradigma orientato agli oggetti. Tutti gli esempi semplicistici di gerarchie di forme sfoggiati come esempi per i nuovi indottrinati sembrano avere un senso logico.

E riutilizzo è la parola del giorno. No ... rendi quell'anno e forse per sempre.

Ho ingoiato tutto questo e mi sono precipitato nel mondo con la mia nuova intuizione.

Banana Monkey Jungle Problem

Con la religione nel cuore e problemi da risolvere, ho iniziato a costruire gerarchie di classi e a scrivere codice. E tutto andava bene per il mondo.

Non dimenticherò mai quel giorno in cui ero pronto a incassare la promessa di Riutilizzare ereditando da una classe esistente. Questo era il momento che stavo aspettando.

È arrivato un nuovo progetto e ho ripensato a quella classe a cui ero così affezionato nel mio ultimo progetto.

Nessun problema. Riutilizzare in soccorso. Tutto quello che devo fare è semplicemente prendere quella Classe dall'altro progetto e usarla.

Beh ... in realtà ... non solo quella classe. Avremo bisogno della classe genitore. Ma ... Ma questo è tutto.

Ugh ... Aspetta ... Sembra che avremo bisogno anche dei genitori dei genitori ... E poi ... Avremo bisogno di TUTTI i genitori. Va bene ... Va bene ... lo gestisco io. Nessun problema.

E fantastico. Ora non verrà compilato. Perché?? Oh, capisco ... Questo oggetto contiene questo altro oggetto. Quindi ne avrò bisogno anche io. Nessun problema.

Aspetta ... Non ho solo bisogno di quell'oggetto. Ho bisogno del genitore dell'oggetto e del genitore del genitore e così via e così via con ogni oggetto contenuto e TUTTI i genitori di ciò che contengono insieme al genitore, al genitore, al genitore ...

Ugh.

C'è una grande citazione di Joe Armstrong, il creatore di Erlang:

Il problema con i linguaggi orientati agli oggetti è che hanno tutto questo ambiente implicito che portano con sé. Volevi una banana, ma quello che hai ottenuto è stato un gorilla con in mano la banana e l'intera giungla.

Banana Monkey Jungle Solution

Posso domare questo problema non creando gerarchie troppo profonde. Ma se l'ereditarietà è la chiave per il riutilizzo, eventuali limiti che porrò su quel meccanismo limiteranno sicuramente i benefici del riutilizzo. Destra?

Destra.

Quindi cosa deve fare un povero programmatore orientato agli oggetti, che ha avuto un valido aiuto con l'aiuto di Kool?

Contenere e delegare. Più su questo più tardi.

Il problema del diamante

Prima o poi, il seguente problema ne risulterà brutto e, a seconda della lingua, irrisolvibile.

La maggior parte delle lingue OO non lo supporta, anche se sembra logico. Cosa c'è di così difficile nel supportare questo nelle lingue OO?

Bene, immagina il seguente pseudocodice:

Class PoweredDevice {
}
Class Scanner eredita da PoweredDevice {
  funzione start () {
  }
}
Class Printer eredita da PoweredDevice {
  funzione start () {
  }
}
Class Copier eredita da Scanner, Stampante {
}

Si noti che sia la classe Scanner che la classe Stampante implementano una funzione chiamata start.

Quindi quale funzione di avvio eredita la classe Copier? Lo scanner uno? Quello della stampante? Non può essere entrambi.

La soluzione di diamante

La soluzione è semplice Non farlo.

Sì, è giusto. La maggior parte delle lingue OO non ti consente di farlo.

Ma, ma ... e se dovessi modellarlo? Voglio il mio riutilizzo!

Quindi è necessario contenere e delegare.

Class PoweredDevice {
}
Class Scanner eredita da PoweredDevice {
  funzione start () {
  }
}
Class Printer eredita da PoweredDevice {
  funzione start () {
  }
}
Class Copier {
  Scanner scanner
  Stampante stampante
  funzione start () {
    printer.start ()
  }
}

Notare qui che la classe Copiatrice ora contiene un'istanza di una stampante e di uno scanner. Delega la funzione di avvio all'implementazione della classe Printer. Potrebbe altrettanto facilmente delegare allo scanner.

Questo problema è ancora un'altra crepa nel pilastro delle eredità.

Il fragile problema della classe base

Quindi sto rendendo le mie gerarchie superficiali e impedendole di essere cicliche. Niente diamanti per me.

E tutto andava bene per il mondo. Questo fino a ...

Un giorno il mio codice funziona e il giorno successivo smette di funzionare. Ecco il kicker. Non ho cambiato il mio codice.

Beh, forse è un bug ... Ma aspetta ... Qualcosa è cambiato ...

Ma non era nel mio codice. Si scopre che il cambiamento è stato nella classe da cui ho ereditato.

Come può un cambiamento nella classe Base infrangere il mio codice ??

Questo è come…

Immagina la seguente classe Base (è scritta in Java, ma dovrebbe essere facile da capire se non conosci Java):

import java.util.ArrayList;
 
Matrice di classe pubblica
{
  Private ArrayList  a = new ArrayList  ();
 
  public void add (elemento Object)
  {
    a.add (elemento);
  }
 
  public void addAll (Object elements [])
  {
    per (int i = 0; i 

IMPORTANTE: notare la riga di codice commentata. Questa linea verrà modificata in seguito, il che spezzerà le cose.

Questa classe ha 2 funzioni sulla sua interfaccia, add () e addAll (). La funzione add () aggiungerà un singolo elemento e addAll () aggiungerà più elementi chiamando la funzione add.

Ed ecco la classe derivata:

ArrayCount di classe pubblica estende Array
{
  conteggio int privato = 0;
 
  @Oltrepassare
  public void add (elemento Object)
  {
    super.add (elemento);
    ++ count;
  }
 
  @Oltrepassare
  public void addAll (Object elements [])
  {
    super.addAll (elementi);
    count + = elements.length;
  }
}

La classe ArrayCount è una specializzazione della classe generale Array. L'unica differenza comportamentale è che ArrayCount tiene un conteggio del numero di elementi.

Vediamo in dettaglio entrambe queste classi.

L'array add () aggiunge un elemento a un ArrayList locale.
L'array addAll () chiama l'aggiunta ArrayList locale per ciascun elemento.

ArrayCount add () chiama add () del suo genitore e quindi incrementa il conteggio.
ArrayCount addAll () chiama addAll () del padre e quindi incrementa il conteggio del numero di elementi.

E tutto funziona bene.

Ora per il cambiamento di rottura. La riga di codice commentata nella classe Base viene modificata come segue:

  public void addAll (Object elements [])
  {
    per (int i = 0; i 

Per quanto riguarda il proprietario della classe Base, funziona ancora come pubblicizzato. E tutti i test automatici continuano a passare.

Ma il proprietario ignora la classe Derivata. E il proprietario della classe Derived è pronto per un brusco risveglio.

Ora ArrayCount addAll () chiama addAll () del suo genitore che chiama internamente add () che è stato OVERRIDEN dalla classe Derived.

Questo fa sì che il conteggio venga incrementato ogni volta che viene chiamato add () della classe Derivata e quindi incrementato NUOVAMENTE dal numero di elementi che sono stati aggiunti nell'adda () della classe Derivata.

È CONTEGGIATO DUE VOLTE.

Se ciò può accadere e ciò accade, l'autore della classe Derived deve SAPERE come è stata implementata la classe Base. E devono essere informati di ogni cambiamento nella classe Base poiché potrebbe interrompere la loro classe Derivata in modi imprevedibili.

Ugh! Questa enorme crepa minaccia per sempre la stabilità del prezioso pilastro dell'eredità.

La fragile soluzione di classe base

Ancora una volta contenere e delegare in soccorso.

Utilizzando Contain and Delegate, passiamo dalla programmazione di White Box alla programmazione di Black Box. Con la programmazione di White Box, dobbiamo esaminare l'implementazione della classe base.

Con la programmazione di Black Box, possiamo ignorare completamente l'implementazione poiché non possiamo iniettare codice nella classe Base sovrascrivendo una delle sue funzioni. Dobbiamo solo preoccuparci dell'interfaccia.

Questa tendenza è inquietante ...

L'ereditarietà avrebbe dovuto essere una grande vittoria per Reuse.

Le lingue orientate agli oggetti non rendono facile contenere e delegare. Sono stati progettati per semplificare l'ereditarietà.

Se sei come me, stai iniziando a chiederti questa cosa dell'ereditarietà. Ma, cosa più importante, questo dovrebbe scuotere la tua fiducia nel potere della Classificazione tramite Gerarchie.

Il problema della gerarchia

Ogni volta che inizio in una nuova società, ho problemi con il problema quando creo un posto dove mettere i miei documenti aziendali, ad es. il manuale dei dipendenti.

Devo creare una cartella denominata Documents e quindi creare una cartella denominata Company in quella?

Oppure creo una cartella denominata Azienda e quindi creo una cartella denominata Documenti in quella?

Entrambi funzionano. Ma quale è giusto? Qual è la migliore?

L'idea delle Gerarchie categoriche era che esistessero Classi Base (genitori) più generali e che Classi Derivate (bambini) fossero versioni più specializzate di quelle classi. E ancora più specializzato mentre percorriamo la catena dell'eredità. (Vedi la Gerarchia delle forme sopra)

Ma se un genitore e un figlio potessero cambiare posto arbitrariamente, allora chiaramente qualcosa non va in questo modello.

La soluzione della gerarchia

Cosa c'è di sbagliato è ...

Le gerarchie categoriche non funzionano.

A cosa servono le gerarchie?

Contenimento.

Se guardi il mondo reale, vedrai gerarchie di contenimento (o proprietà esclusiva) ovunque.

Ciò che non troverai sono le gerarchie categoriche. Lascialo affondare per un momento. Il paradigma orientato agli oggetti era basato sul mondo reale, uno pieno di oggetti. Ma poi usa un modello rotto, vale a dire. Gerarchie categoriche, dove non esiste analogia nel mondo reale.

Ma il mondo reale è pieno di Gerarchie di Contenimento. Un ottimo esempio di gerarchia di contenimento sono i calzini. Sono in un cassetto per calze che è contenuto in un cassetto del comò che è contenuto nella tua camera da letto che è contenuto nella tua casa, ecc.

Le directory sul disco rigido sono un altro esempio di gerarchia di contenimento. Contengono file.

Quindi come classificare allora?

Bene, se pensi ai Documenti dell'azienda, praticamente non importa dove li ho messi. Posso metterli in una cartella di documenti o in una cartella chiamata Stuff.

Il modo in cui lo categorizzo è con i tag. Taggo il file con i seguenti tag:

Documento
Azienda
Manuale

I tag non hanno ordine o gerarchia. (Questo risolve anche il problema del diamante.)

I tag sono analoghi alle interfacce poiché è possibile avere più tipi associati al documento.

Ma con così tante crepe, sembra che il pilastro dell'ereditarietà sia caduto.

Arrivederci, eredità.

Incapsulamento, il secondo pilastro a cadere

A prima vista, l'incapsulamento sembra essere il secondo più grande vantaggio della programmazione orientata agli oggetti.

Le variabili di stato dell'oggetto sono protette dall'accesso esterno, ovvero sono incapsulate nell'oggetto.

Non dovremo più preoccuparci delle variabili globali a cui accedono chi sa chi.

L'incapsulamento è sicuro per le tue variabili.

Questa cosa dell'incapsulamento è INCREDIBILE !!

Lunga vita all'incapsulamento ...

Questo fino a ...

Il problema di riferimento

Per motivi di efficienza, gli oggetti vengono passati alle funzioni NON per valore ma per riferimento.

Ciò significa che le funzioni non passeranno l'oggetto, ma passeranno invece un riferimento o un puntatore all'oggetto.

Se un oggetto viene passato per riferimento a un costruttore di oggetti, il costruttore può inserire quel riferimento di oggetto in una variabile privata protetta da incapsulamento.

Ma l'oggetto passato NON è sicuro!

Perchè no? Perché qualche altro pezzo di codice ha un puntatore all'oggetto, vale a dire. il codice che chiamava il costruttore. DEVE avere un riferimento all'oggetto, altrimenti non potrebbe passarlo al costruttore?

La soluzione di riferimento

Il costruttore dovrà clonare l'oggetto passato. E non un clone superficiale ma un clone profondo, cioè ogni oggetto contenuto nell'oggetto passato e ogni oggetto in quegli oggetti e così via e così via.

Questo per quanto riguarda l'efficienza.

Ed ecco il kicker. Non tutti gli oggetti possono essere clonati. Ad alcuni sono associate risorse del sistema operativo che rendono la clonazione inutile o nella migliore delle ipotesi impossibile.

E OGNI singolo linguaggio OO tradizionale ha questo problema.

Arrivederci, incapsulamento.

Polimorfismo, il terzo pilastro a cadere

Il polimorfismo era il figliastro dai capelli rossi della Trinità orientata agli oggetti.

È una specie di Larry Fine del gruppo.

Ovunque andassero, lui era lì, ma era solo un personaggio secondario.

Non è che il polimorfismo non sia eccezionale, è solo che non è necessario un linguaggio orientato agli oggetti per ottenerlo.

Le interfacce ti daranno questo. E senza tutto il bagaglio di OO.

E con le interfacce, non esiste un limite al numero di comportamenti diversi che è possibile combinare.

Quindi, senza troppi indugi, salutiamo il polimorfismo OO e salutiamo il polimorfismo basato sull'interfaccia.

Promesse non mantenute

Bene, OO ha sicuramente promesso molto all'inizio. E queste promesse vengono ancora fatte ai programmatori ingenui seduti in classe, a leggere blog e a seguire corsi online.

Mi ci sono voluti anni per capire come OO mi ha mentito. Anch'io avevo gli occhi spalancati, inesperto e fiducioso.

E mi sono bruciato.

Arrivederci, programmazione orientata agli oggetti.

Quindi cosa?

Ciao, programmazione funzionale. È stato così bello lavorare con te negli ultimi anni.

Solo per quello che sai, NON prendo nessuna delle tue promesse al valore nominale. Dovrò vederlo per crederci.

Una volta bruciato, due volte timido e tutto.

Capisci.

Se ti è piaciuto, fai clic su in basso in modo che altre persone lo vedano qui su Medium.

Se vuoi unirti a una comunità di sviluppatori web che si apprendono e si aiutano a vicenda nello sviluppo di app Web utilizzando la Programmazione funzionale in Elm, controlla il mio gruppo Facebook, Impara la programmazione Elm https://www.facebook.com/groups/learnelm/

Il mio Twitter: @cscalfani