I principali paradigmi di programmazione

Questo è il post #10 di 12 nella serie “Fondamenti di Programmazione 101

Se guardiamo a tutti i linguaggi di programmazione, possiamo vedere dei patterns e delle similarità fra di loro e questo ci può aiutare a classificarli in differenti paradigmi.

Il motivo per il quale è utile fare questa classificazione è perché il modo in cui scriviamo un programma in uno di questi paradigmi cambia notevolmente da come lo scriveremmo in un linguaggio appartenente a un diverso gruppo.

Introduciamo la programmazione strutturale

La programmazione strutturale è quella che abbiamo analizzato in questa serie di articoli. Cicli, condizioni e funzioni definiscono il flusso del programma che usa questo specifico paradigma.

La programmazione strutturata è un ramo della famiglia dei paradigmi chiamati programmazione imperativa.

Linguaggi che usano concetti di programmazione iterativa utilizzano dichiarazioni per cambiare lo stato di un programma.

Dichiarazioni

Una dichiarazione può essere vista come un comando che diamo all’applicazione. Adesso è ora di analizzare cosa intendiamo quando parliamo dello stato di un applicazione.

Lo stato di un programma

Si dice che un programma ha uno stato, quando tiene traccia degli eventi che si sono verificati. Un programma salva dei dati nelle variabili e a un dato punto nel tempo, durante l’esecuzione dello stesso, possiamo ispezionare i dati che sono stati definiti in tutte le sue variabili. L’unione dei valori salvati in tutte queste variabili costituiscono lo stato del programma.

Una comparazione fra la programmazione imperativa e quella strutturale

Programmi scritti in un linguaggio come l’ assembly utilizzano un concetto conosciuto come il GOTO che è una tecnica per controllare il flusso del programma. Per utilizzarlo, si inseriscono nel programma delle etichette (chiamate labels) nel codice che ci permettono di “saltare” a una di queste labels e continuare l’esecuzione del programma da quel punto. Usando questo stile di programmazione è quello che abbiamo definito come programmazione imperativa.

Con l’evoluzione dei linguaggi di programmazione e l’introduzione di altri costrutti come i cicli, le dichiarazioni e le funzioni, è nata l’esigenza di distinguere questi linguaggi che usano un paradigma più moderno da quelli più vecchi. L’idea di fondo rimane la stessa di quella usata, per esempio, nell’ assembly visto che questi nuovi tools cambieranno lo stato del nostro programma ma senza affidarci all’uso del GOTO, che intrinsecamente ha notevoli svantaggi, portando a quello che viene definito spaghetti code!.

Questo cambio nel paradigma è stato ciò che ha portato all’introduzione del termine programmazione strutturata. Un linguaggio che supporta questo paradigma è un linguaggio che modifica lo stato di un programma usando dichiarazioni, cicli e funzioni.

Alle volte vengono anche usati i termini, programmazione strutturale o modulare.

Concetti di programmazione orientata agli oggetti

L’idea alla base dei linguaggi di programmazione orientati agli oggetti è quella di modellare il codice alla stessa maniera di come la mente umana guarda al mondo.

Se ci hai mai pensato, noi sempre classifichiamo cose e li raggruppiamo insieme usando delle astrazioni. Quando parliamo delle auto, abbiamo sempre una conoscenza condivisa riguardo quello che è incluso in questa categoria e moto, case, cani e aerei non rientrano in questa categoria.

Raggruppare cose in questo tipo di astrazioni, rende le nostra vita più semplice perché non abbiamo bisogno di scendere nei dettagli specifici ogni volta che parliamo riguardo qualcosa.

Cose all’interno di queste astrazioni possono essere molto diverse fra loro ma tutte condividono caratteristiche comuni che ognuno di noi può comprendere.

Facciamo un esempio concreto riprendendo l’idea dell’auto. Quando diciamo che un auto sta accelerando, tutti capiamo che sta aumentando la propria velocità e la stessa cosa vale se facciamo riferimento a quando freniamo. Quindi è un concetto astratto comune a tutte le auto e che tutti comprendiamo, ma l’accelerazione sarà diversa fra una macchina sportiva e un utilitaria.

Per comprendere i concetti alla base della programmazione orientata agli oggetti, dobbiamo analizzare e comprendere i suoi concetti base.

Classi e oggetti

Nella programmazione a oggetti, una classe è come una planimetria di una casa (spesso incontrerete il termine blueprint). Prendiamo il concetto di una persona per fare un esempio concreto.

Come possiamo descrivere una persona? Possiamo iniziare facendo una lista di cose che possiamo applicare a tutte le persone.

Una persona può avere la seguente lista di attributi:

  • nome
  • età
  • sesso
  • altezza
  • peso
  • colore dei capelli
  • colore degli occhi
  • taglia di scarpe
  • nazionalità
  • Indirizzo
  • numero di telefono

Quindi possiamo decidere che questi attributi si possono applicare a tutte le persone, questi attributi rappresentano i dati di una persona.
Fino a questo momento, però, non abbiamo descritto alcun comportamento. Possiamo creare una lista anche per i comportamenti che tutte le persone possono avere.

Una persona può fare:

  • Camminare
  • Correre
  • Saltare
  • Sedersi
  • Alzarsi
  • Dormire
  • Lavorare
  • Giocare
  • Ballare

Una cosa importante da capire è questo, se abbiamo bisogno di usare una classe persona all’interno del nostro programma, non necessariamente dobbiamo enumerare tutti i possibili attributi e comportamenti che una persona può avere ma soltanto quelli che servono nel nostro programma.

Graficamente possiamo rappresentare la nostra classe persona nel seguente modo:

Person - Class Diagram

L’immagine precedente rappresenta una versione semplificata della classe persona che abbiamo definito precedentemente, composta da un rettangolo con tre sezioni.
Nella prima sezione abbiamo il nome che abbiamo dato alla nostra classe. Nella sezione centrale descriviamo i dati
che fanno parte della classe persona, quindi i suoi attributi. L’ultima sezione rappresenta di comportamenti cioè i
metodi esposti dalla classe.
Questa rappresentazione grafica prende il nome di class diagram nel linguaggio UML.

Al momento lasceremo la sezione dei comportamenti vuota.

Considerato che il modello di programmazione orientata agli oggetti è molto focalizzato sui dati, spesso questo sarà il nostro punto di partenza. Inoltre, molto spesso capiterà che i dati che decidiamo d’includere all’interno delle
nostre classi guideranno anche i comportamenti che includeremo in quanto i comportamenti agiscono sui dati.

A questo punto abbiamo definito il blueprint per la classe persona ma non abbiamo rappresentato nessuna persona specifica. La rappresentazione di una cosa, nel nostro caso una persona, è chiamata un oggetto. Un oggetto apparterrà sempre a una classe.

Adesso che abbiamo la classe persona, possiamo creare un oggetto che rappresenterà una persona specifica.

Supponiamo di avere un gruppo di persone che vogliamo rappresentare nel nostro programma, graficamente possiamo avere qualcosa del genere:

Esempio di oggetti di tipo Person

Come possiamo vedere dal diagramma precedente, tutti e quattro gli oggetti hanno il loro specifico set di dati. I dati di un oggetto specifico sono indipendenti dai dati degli altri oggetti che appartengono alla stessa classe. Se cambiamo l’indirizzo in un oggetto, non cambierà i dati negli altri oggetti perché sono specifici per quel dato oggetto.

Ricapitolando possiamo dire che una classe è un blueprint generico per gli oggetti. I dati che definiamo all’interno della classe vengono spesso definiti come le sue variabili membro (member variables) o attributi (attributes).

Member variables

Una member variable è come qualunque altra variabile con la sola differenza che vive all’interno di un oggetto.

Come buona pratica di programmazione, i linguaggi orientati agli oggetti definiscono che una member variable dovrebbe essere incapsulata all’interno dell’oggetto e accessi diretti a queste variabili, dall’esterno della classe, dovrebbe essere prevenuti.

Cos’è’ l’incapsulamento?

Incapsulamento, anche conosciuto come information hiding, è il concetto attraverso il quale l’implementazione
interna di un oggetto è nascosta da ogni cosa all’esterno dell’oggetto stesso.

Ci sono vari modi per descrivere il concetto dell’incapsulamento. Vediamo come due ingegneri informatici Americani,
James Rumbaugh e Michael Blaha lo descrivono:

One design goal is to treat classes as “black boxes,” whose external interface is public but whose internal details are hidden from view. Hiding internal information permits implementation of a class to be changed without
requiring any clients of the class to modify code.

La parola chiave è interfaccia, un interfaccia è quello che usiamo per comunicare con un oggetto. L’unica cosa che dobbiamo capire per comunicare con un oggetto è l’interfaccia che espone. Quello che dovremmo nascondere dal mondo estero sono i dati dell’oggetto.

Ma come possiamo nascondere i dati di un oggetto?

I dati di un oggetto possono essere nascosti al mondo esterno, cioè a chi interagisce con l’oggetto, usando la parola chiave private.

Definendo gli attributi di una classe privati, gli stessi attributi possono solo essere acceduti dall’interno della
classe e non dall’esterno. Per esempio:

Incapsulamento - Esempio

In questo modo, gli attributi name e age non possono essere acceduti dall’esterno della classe in quanto sono
stati definiti privati.

La classe Person, per come è definita adesso, è praticamente inutile in quanto possiamo creare un oggetto e
assegnare dei valori iniziali ma non c’è alcun modo di fare nient’altro dato che non possiamo accedere agli attributi!

Abbiamo bisogno di creare un interfaccia che ci consentirà di accedere ai valori dell’oggetto e questo può essere
fatto attraverso l’uso dei metodi di classe.

Metodi di classe

Un metodo di classe non è niente di più di una funzione che appartiene alla classe.

Il motivo per il quale abbiamo nomi diversi per queste funzioni è soltanto quello di differenziarli tra una funzione che è parte della classe e una che non lo è.

Due metodi che troviamo, quasi sempre, nella definizione di una classe sono quelli definiti come getters e setters.

Vediamo come cambia il nostro ultimo esempio utilizzando i getters e setters:

Getters e Setters - Esempio

In questo modo possiamo accedere alle variabili membro definite nella classe. Adesso possiamo creare oggetti, assegnare nuovi valori ed ottenere i valori settati nell’oggetto. Per esempio:

Getters e Setters - Esempio 2

Questo snippet di codice produrrà:

Paolo Rossi ha 40 anni.
Paolo Rossi ha 35 anni.

Mantenendo lo stato dell’oggetto interno nascosto e fornendo dei getters e/o setters abbiamo più controllo su cosa può essere modificato.

Un classico esempio per rinforzare questo concetto è il seguente. Se avessimo l’attributo age pubblico e quindi direttamente modificabile dall’esterno, non avremmo alcun modo per fare dei controlli sulla validità del valore settato quindi non ci sarebbe modo di evitare che venga impostata un età con un valore negativo, cosa ovviamente errata.

Utilizzando un setter potremmo scrivere la seguente logica:

Funzione setter - Esempio

e adesso abbiamo un controllo maggiore e possiamo anche rilanciare un errore nel caso venga fornito un valore invalido.

Adesso possiamo completare il nostro class diagram per la classe Person:

Classe Person - Esempio

Ereditarietà

L’Ereditarietà è uno dei tre pilastri portanti dei linguaggi di programmazione orientata agli oggetti insieme con l’incapsulamento e il polimorfismo.

Facciamo subito un esempio per cercare di spiegare questo importante concetto.

Supponiamo che io ti chieda se posso prendere in prestito un telefono perché ho la necessità di fare una chiamata. In verità non farebbe una grande differenza se tu mi prestassi il tuo smartphone, un vecchio telefono cellulare o addirittura un telefono fisso. Tutti e tre condividono delle caratteristiche comuni fra cui quella di effettuare una chiamata telefonica.

Potremmo immaginare questo scenario come formato da una serie di livelli di astrazione dove abbiamo una relazione fra i livelli. Questo è quello che chiamiamo una relazione is-a (cioè è un). Possiamo illustrare questa relazione nel seguente modo:

Telefoni - Esempio di una relazione

Analizzando il grafico possiamo dire, visto che uno smartphone è un telefono cellulare, può fare tutto quello che un telefono cellulare può fare. Inoltre sappiamo anche che uno smartphone può fare cose che un telefono cellulare non può fare, come per esempio utilizzare il GPS e una mappa per essere utilizzato come un navigatore. Alla stessa maniera, un telefono cellulare può fare tutto quello che un telefono fisso può fare, cioè fare e ricevere chiamate ma inoltre può inviare e ricevere SMS, cosa che un telefono fisso non può fare.

Possiamo anche notare che questa relazione può essere vista come una relazione padre-figlio (parent-child). Lo smartphone è figlio del telefono cellulare e il telefono cellulare è il padre. Questo tipo di relazione, oltretutto, significa che il figlio erediterà dal padre ed è questo il modo in cui l’ereditarietà funziona in un linguaggio di programmazione orientato agli oggetti.

Una classe può ereditare da un altra classe e così facendo, erediterà tutto quello che è definito nella classe padre con la possibilità di aggiungere solo quello che è necessario nella classe figlia.

Facciamo un esempio riutilizzando la classe Person che abbiamo definito precedentemente:

Class Diagram Person - Esempio 2

Come possiamo vedere, la classe Person è definita con tutti i suoi attributi dichiarati privati e i getters e setters sono pubblici.

Come già discusso precedentemente, tutte le proprietà definite si applicheranno a tutte le persone ma possiamo avere delle persone che possono necessitare di ulteriori attributi.

Un esempio potrebbe essere se avessimo l’esigenza di creare una classe per rappresentare un impiegato. Gli impiegati sono persone e quindi hanno la necessità di avere tutti gli attributi che abbiamo definito nella classe Person ma inoltre abbiamo l’esigenza di aggiungere attributi specifici dell’impiegato come per esempio il salario e il dipartimento.

Ovviamente non vogliamo creare una nuova classe ripetendo tutti gli attributi della classe Person come sotto dimostrato:

Person - Employee - Class Diagram

Una soluzione decisamente migliore sarebbe quella di dire che la classe Employee eredita dalla classe Person e così facendo erediterebbe tutto quello definito nella classe Employee.

Vediamo la rappresentazione UML del class diagram:

Esempio di ereditarietá attraverso le classi

Quando dobbiamo implementare l’eredita nel nostro codice, non dobbiamo effettuare alcuna modifica nella classe Person. Vediamo come implementare la classe Employee:

Classe Employee - Esempio di Codice

In pseudo codice utilizziamo la parola chiave inherit ma ogni linguaggio utilizza una propria convenzione (spesso la parola chiava extends)

Esistono altri paradigmi di programmazione che non tratteremo in questo articolo per non complicare eccessivamente l’argomento.