Articolo originale: https://www.freecodecamp.org/news/object-oriented-programming-concepts-21bb035f7260/

Hai notato come durante i colloqui di lavoro vengono fatte sempre le stesse domande, ancora e ancora?

Sono sicuro che sai cosa voglio dire.

Ad esempio:

Dove ti vedi tra cinque anni?

o ancora peggio:

Quale pensi che sia il tuo più grande punto debole?

Datemi tregua. Rispondere a questo domanda è un punto debole! In ogni caso, non è questo il punto.

Saranno anche delle domande banali, ma sono importanti perché danno delle informazioni su di te: il tuo stato d'animo attuale, la tua mentalità, il tuo punto di vista.

Quando rispondi, dovresti fare attenzione, per evitare di dire cose di cui potresti pentirti.

Oggi voglio parlare di una domanda di una tipologia simile a questa nel mondo della programmazione:

Quali sono i principi fondamentali della programmazione orientata agli oggetti?

Mi sono trovato da entrambi i lati di questa domanda. È uno di quegli argomenti che vengono chiesti spesso e che non puoi permetterti di non conoscere.

Di solito, gli sviluppatori junior ed entry-level si trovano a dover rispondere a questa domanda, perché è un modo per chi gestisce il colloquio per capire tre cose:

  1. Il candidato si è preparato per il colloquio?
    Punti bonus per una risposta immediata — dimostra un approccio serio.
  2. Il candidato ha passato la fase tutorial?
    Capire i principi della programmazione orientata agli oggetti (OOP) dimostra di essere andati oltre il copia e incolla dai tutorial e di vedere le cose da una prospettiva più alta.
  3. Il candidato possiede una comprensione profonda o superficiale?
    Il livello di competenza su questo argomento spesso eguaglia quello di molti altri. Fidati.

I quattro principi della programmazione orientata agli oggetti sono incapsulamento, astrazione, ereditarietà e polimorfismo.

Queste parole potrebbe spaventare uno sviluppatore principiante. E, nel complesso, le spiegazioni eccessivamente lunghe su Wikipedia a volte raddoppiano la confusione.

Ed è per questo che voglio fornire una spiegazione semplice, breve e chiara per ognuno di questi concetti. Potrebbe sembrare qualcosa da spiegare a un bambino, ma in realtà è proprio ciò che vorrei sentirmi rispondere durante un colloquio.

Incapsulamento

Diciamo di avere un programma con pochi oggetti che hanno diverse logiche e che possono comunicare tra di loro – secondo le regole definite nel programma.

L'incapsulamento viene raggiunto quando ogni oggetto mantiene privato il suo stato all'interno di una classe. Gli altri oggetti non hanno accesso diretto al suo stato, ma possono soltanto chiamare una lista di funzioni pubbliche – chiamate metodi.

Quindi, l'oggetto gestisce il suo stato attraverso dei metodi – e nessuna altra classe può modificarlo se non autorizzata esplicitamente. Se vuoi comunicare con l'oggetto, dovresti usare i metodi forniti, ma (di default) non puoi cambiarne lo stato.

Diciamo che stai costruendo un piccolo gioco di simulazione. Ci sono delle persone e c'è un gatto, che comunicano tra di loro. Vogliamo applicare l'incapsulamento, quindi incapsuliamo tutta la logica del gatto nella classe Cat. Potrebbe avere questo aspetto:

M4t8zW9U71xeKSlzT2o8WO47mdzrWkNa4rWv
Puoi dare da mangiare al gatto (usando il metodo pubblico Feed), ma non puoi andare a modificare direttamente quanto è affamato (hungry).

In questo caso, lo "stato" del gatto è costituito dalle variabili private mood, hungry, energy e dal metodo privato meow(), che il gatto può chiamare ogni volta che vuole, mentre le altre classi non possono.

Ciò che possono fare è definito nei metodi pubblici sleep(), play() e feed(). Ognuno di questi modifica in qualche modo lo stato interno e può invocare meow(). Quindi, viene stabilito un legame tra lo stato privato e i metodi pubblici.

Questo è l'incapsulamento.

Astrazione

L'astrazione può essere intesa come un'estensione naturale dell'incapsulamento.

Nel design orientato agli oggetti, i programmi sono spesso estremamente estesi e oggetti separati comunicano molto tra di loro. Quindi gestire un codebase così esteso per anni – senza cambiamenti strada facendo – è complicato.

L'astrazione è un concetto finalizzato a semplificare questo problema.

Applicare un'astrazione significa che ogni oggetto dovrebbe mettere in mostra solo un meccanismo di alto livello.

Questo meccanismo dovrebbe nascondere i dettagli interni di implementazione, rivelando solamente le operazioni rilevanti per gli altri oggetti.

Pensa a una macchina del caffè. Fa un sacco di roba e produce rumori bizzarri dal suo interno. Ma tutto ciò che devi fare è mettere dentro il caffè e premere un bottone.

Preferibilmente, questo meccanismo dovrebbe essere facile da utilizzare e dovrebbe cambiare di rado. Pensa a un piccolo insieme di metodi pubblici che ogni altra classe può chiamare senza "conoscere" come funzionano.

Un altro esempio pratico di astrazione?
Pensa a come usi il tuo cellulare:

hiX0NQOcZFShroq-a3FM5pFP2LV4UUI5mLle
I telefoni cellulari sono complessi, ma usarli è semplice.

Interagisci con il tuo cellulare usando solamente pochi pulsanti. Cosa accade al suo interno? Non c'è bisogno che tu lo sappia – i dettagli di implementazione sono nascosti. Devi soltanto conoscere una breve lista di azioni.

I cambiamenti nell'implementazione – ad esempio, un aggiornamento del software – raramente influiscono sull'astrazione di cui fai uso.

Ereditarietà

Bene, abbiamo visto come l'incapsulamento e l'astrazione possono aiutarci a sviluppare e mantenere un codebase esteso.

Ma sai qual è un altro problema comune del design della programmazione orientata agli oggetti?

Spesso gli oggetti sono molto simili. Condividono una logica comune ma non sono completamente uguali.

Dunque, come possiamo riutilizzare una logica comune ed estrarre la logica unica in una classe separata? Un modo per farlo è tramite l'ereditarietà.

Questo si traduce nella creazione di una classe (figlia) derivata da un altra classe (genitore), formando una gerarchia.

La classe figlia riutilizza tutti i campi e i metodi della classe genitore (le parti comuni) e può implementarne di propri (parte unica).

Ad esempio:

ZIm7lFjlrKeMWxcH8fqBapNkuSJIxW9-t9yf
Un insegnante privato è un tipo di insegnante, e ogni insegnante è un tipo di persona.

Se il nostro programma ha bisogno di gestire insegnanti pubblici e privati, ma anche altre persone come studenti, può implementare questa gerarchia di classi.

In questo modo, a ogni classe si aggiunge soltanto ciò che le è necessario riutilizzando la logica comune della classe genitore.

Polimorfismo

Ed eccoci alla parola più complicata! Polimorfismo deriva dal greco e vuol dire "molte forme".

Conosciamo già il potere  dell'ereditarietà e lo sfruttiamo volentieri. Ma ecco che arriva un problema.

Ipotizziamo di avere una classe genitore e poche classi figlie che ereditano proprietà e metodi da questa. A volte potremmo aver bisogno di usare una collezione – ad esempio una lista – che contiene un mix di tutte queste classi. Oppure abbiamo un metodo implementato per la classe genitore – ma vorremmo usarlo anche per i discendenti.

Si può risolvere tutto ciò grazie al polimorfismo.

In parole povere, il polimorfismo fornisce un modo di usare una classe esattamente come il suo genitore, così che non ci sia confusione data dal mix dei vari tipi. Ma ogni classe figlia mantiene i propri metodi tal quali.

Questo avviene tipicamente definendo un'interfaccia (genitore) da riutilizzare. Definisce un gruppo di metodi comuni. Poi, ogni classe figlia implementa la propria versione di questi metodi.

Ogni volta che una collezione (come una lista) o un metodo si aspetta una istanza del genitore (in cui sono definiti i metodi comuni), il linguaggio si occupa di valutare l'implementazione corretta per il metodo comune – indipendentemente da quale figlio gli viene passato.

Dai un'occhiata allo schizzo dell'implementazione delle figure geometriche qui sotto. Riutilizzano un'interfaccia comune per calcolare l'area e il perimetro:

8GySv1U8Kh9nVVyiTqv5cDuWZC7p0uARVeF0
Triangolo, Cerchio e Rettangolo possono essere usati nella stessa collezione.

Avere tre figure che ereditano Figure Interface dal genitore, ti permette di creare una lista mista di triangle, circle e rectangle, che puoi trattare come lo stesso tipo di oggetto.

Poi, se questa lista prova a calcolare l'area di un elemento, il metodo corretto viene trovato ed eseguito. Se l'elemento è un triangolo, viene chiamata CalculateSurface() del triangolo. Se è un cerchio, viene chiamata CalculateSurface() del cerchio, e così via.

Se hai una funzione che opera con una figura usando i suoi parametri, non devi definirla tre volte – una volta per il triangolo, una volta per il cerchio e una volta per il rettangolo.

Puoi definirla una volta e accettare Figura come argomento. Se passi un triangolo, un cerchio o un rettangolo – finché implementano CalculateParamter() – il loro tipo non conta.

Spero che questo sia stato d'aiuto. Puoi usare direttamente queste stesse spiegazioni in un colloquio di lavoro.