Cos’è la programmazione orientata agli oggetti? OOP spiegata nel dettaglio
Articolo in lingua originale di The Educative Team
La programmazione orientata agli oggetti (OOP) è un paradigma fondamentale della programmazione utilizzato da quasi tutti gli sviluppatori a un certo punto della loro carriera. L’OOP è il paradigma di programmazione più diffuso per lo sviluppo di software e viene insegnato alla maggior parte dei programmatori come metodo standard per programmare durante la loro formazione. Un altro paradigma di programmazione molto diffuso è la programmazione funzionale, ma non ne parleremo ora.
Oggi analizzeremo le basi di ciò che rende un programma orientato agli oggetti, in modo che possiate iniziare a utilizzare questo paradigma nei vostri algoritmi, progetti e colloqui.
Ora tuffiamoci in questi concetti e tutorial OOP!
Cosa è la programmazione orientata agli oggetti?
La programmazione orientata agli oggetti (OOP) è un paradigma di programmazione in informatica che si basa sul concetto di classi e oggetti. Si usa per strutturare un programma software in pezzi di codice semplici e riutilizzabili (di solito chiamati classi), che vengono usati per creare istanze individuali di oggetti. Esistono molti linguaggi di programmazione orientati agli oggetti, tra cui JavaScript, C++, Java e Python.
I linguaggi OOP non sono necessariamente limitati al paradigma di programmazione orientato agli oggetti. Alcuni linguaggi, come JavaScript, Python e PHP, consentono di utilizzare stili di programmazione sia procedurali che orientati agli oggetti.
Una classe è un progetto astratto che crea oggetti concreti più specifici. Le classi spesso rappresentano categorie ampie, come Auto o Cane, che condividono attributi. Queste classi definiscono quali attributi avrà un’istanza di questo tipo, come il colore, ma non il valore di tali attributi per un oggetto specifico.
Le classi possono anche contenere funzioni chiamate metodi, disponibili solo per gli oggetti di quel tipo.
Queste funzioni sono definite all’interno della classe ed eseguono un’azione utile per quel tipo specifico di oggetto.
Ad esempio, la classe Automobile potrebbe avere un metodo ricolora che cambia l’attributo colore della macchina. Questa funzione è utile solamente per gli oggetti di tipo Automobile, per cui la dichiariamo all’interno della classe Automobile, rendendola un metodo.
I template delle classi sono usati come modello per creare oggetti individuali. Questi rappresentano esempi specifici della classe astratta, come myCar o goldenRetriever. Ogni oggetto può avere valori unici per le proprietà definite nella classe.
Supponiamo per esempio di avere creato la classe Automobile in modo che contenga tutte le proprietà che una macchina deve avere, colore, marchio e modello. Successivamente creiamo un’istanza myCar di un oggetto di tipo Automobile per rappresentare la nostra macchina specifica.
Possiamo poi impostare il valore delle proprietà definite nella classe per descrivere la nostra macchina senza influenzare altri oggetti o il template della classe. Possiamo utilizzare questa classe per rappresentare qualsiasi numero di automobili.
Benefici dell’OOP per ingegneria del software
- OOP modella cose complesse rendendole strutture semplici e riproducibili
- Gli oggetti OOP riutilizzabili possono essere utilizzati in tutti i programmi
- Il polimorfismo permette comportamenti specifici per la classe
- Più facile fare il debug, le classi spesso contengono tutte le informazioni a loro applicabili
- Protegge in modo sicuro le informazioni sensibili attraverso l’incapsulamento
Come strutturare programmi OOP
Prendiamo un problema del mondo reale e progettiamo concettualmente un programma software OOP.
Immaginate di gestire un campo di dog-sitting con centinaia di animali domestici e di tenere traccia dei nomi, delle età e dei giorni di presenza di ciascun animale.
Come si potrebbe progettare un software semplice e riutilizzabile per modellare i cani?
Con centinaia di cani, sarebbe inefficiente scrivere voci uniche per ogni cane, perché si scriverebbe molto codice ridondante. Di seguito vediamo come potrebbe apparire con gli oggetti rufus e fluffy.
//Object of one individual dog var rufus = { name: "Rufus", birthday: "2/1/2017", age: function() { return Date.now() - this.birthday; }, attendance: 0 } //Object of second individual dog var fluffy = { name: "Fluffy", birthday: "1/12/2019", age: function() { return Date.now() - this.birthday; }, attendance: 0 }
Come si può vedere sopra, c’è molto codice duplicato tra i due oggetti. La funzione age() compare in ogni oggetto. Poiché vogliamo le stesse informazioni per ogni cane, possiamo utilizzare oggetti e classi.
Raggruppare informazioni correlate per formare una struttura a classi rende il codice più corto e più facile da mangenere.
Nell’esempio del dogsitting, ecco come un programmatore potrebbe pensare di organizzare una OOP:
- Creare una classe per tutti i cani come schema di informazioni e comportamenti (metodi) che tutti i cani avranno, indipendentemente dal tipo. Questa è nota anche come classe madre.
- Creare sottoclassi per rappresentare diverse sottocategorie di cani all’interno del progetto principale. Queste sono anche chiamate classi figlie.
- Aggiungere attributi e comportamenti unici alle classi figlie per rappresentare le differenze.
- Creare oggetti dalla classe figlia che rappresentino i cani all’interno di quel sottogruppo.
Il diagramma seguente rappresenta come progettare un programma OOP raggruppando i dati e i comportamenti relativi per formare un modello semplice e creando poi sottogruppi per dati e comportamenti specializzati.
La classe Cane è un modello generico che contiene come attributi solo la struttura dei dati e dei comportamenti comuni a tutti i cani.
Creiamo quindi due classi figlie di Dog, HerdingDog e TrackingDog. Queste hanno i comportamenti ereditati da Dog (bark()), ma anche comportamenti unici per i cani di quel sottotipo. Infine, creiamo oggetti del tipo HerdingDog per rappresentare i singoli cani Fluffy e Maisel.
Creare blocchi di OOP
Classi
In poche parole, le classi sono essenzialmente tipi di dati definiti dall’utente. Le classi sono il luogo in cui si crea un modello per la struttura dei metodi e degli attributi. I singoli oggetti vengono istanziati a partire da questa struttura.
Le classi contengono campi per gli attributi e metodi per i comportamenti. Nell’esempio della nostra classe Cane, gli attributi includono nome e compleanno, mentre i metodi includono bark() e updateAttendance().
Ecco un frammento di codice che dimostra come programmare una classe Cane utilizzando il linguaggio JavaScript.
class Dog { constructor(name, birthday) { this.name = name; this.birthday = birthday; } //Declare private variables _attendance = 0; getAge() { //Getter return this.calcAge(); } calcAge() { //calculate age using today's date and birthday return Date.now() - this.birthday; } bark() { return console.log("Woof!"); } updateAttendance() { //add a day to the dog's attendance days at the petsitters this._attendance++; } }
Ricordate, la classe è uno schema per modellare un cane e un oggetto è istanziato dalla classe e rappresenta un oggetto individuale del mondo reale.
Oggetti
Gli oggetti sono, senza dubbio, una parte importante dell’OOP! Gli oggetti sono istanze di una classe create con dati specifici. Per esempio, nel frammento di codice qui sotto, Rufus è un’istanza della classe Dog.
class Dog { constructor(name, birthday) { this.name = name; this.birthday = birthday; } //Declare private variables _attendance = 0; getAge() { //Getter return this.calcAge(); } calcAge() { //calculate age using today's date and birthday return Date.now() - this.birthday; } bark() { return console.log("Woof!"); } updateAttendance() { //add a day to the dog's attendance days at the petsitters this._attendance++; } } //instantiate a new object of the Dog class, and individual dog named Rufus const rufus = new Dog("Rufus", "2/1/2017");
Quando la classe Dog viene chiamata:
- Un nuovo oggetto viene creato a nome rufus
- Il costruttore esegue gli argomenti nome e birthday assegnando valori
Vocabolario di programmazione
In JavaScript, gli oggetti sono un tipo di variabile. Questo può creare confusione perché gli oggetti possono essere dichiarati senza un modello di classe in JavaScript, come mostrato all’inizio.
Gli oggetti hanno stati e comportamenti. Lo stato di un oggetto è definito dai dati: cose come nome, data di nascita e altre informazioni che si vorrebbero salvare riguardo a un cane. I comportamenti sono metodi che l’oggetto può intraprendere.
Attributi
Gli attributi sono le informazioni che vengono memorizzate. Gli attributi sono definiti nel modello di classe. Quando gli oggetti vengono istanziati, i singoli oggetti contengono dati memorizzati nel campo Attributi.
Lo stato di un oggetto è definito dai dati contenuti nei campi degli attributi dell’oggetto. Ad esempio, un cucciolo e un cane potrebbero essere trattati in modo diverso in un campo per animali. Il compleanno può definire lo stato di un oggetto e consentire al software di gestire in modo diverso cani di età diverse.
Metodi
I metodi rappresentano i comportamenti. I metodi eseguono azioni; possono restituire informazioni su un oggetto o aggiornare i dati di un oggetto. Il codice del metodo è definito nella definizione della classe.
Quando i singoli oggetti vengono istanziati, questi possono chiamare i metodi definiti nella classe. Nel frammento di codice sottostante, il metodo bark è definito nella classe Cane e il metodo bark() viene richiamato sull’oggetto Rufus.
class Dog { //Declare protected (private) fields _attendance = 0; constructor(name, birthday) { this.namee = name; this.birthday = birthday; } getAge() { //Getter return this.calcAge(); } calcAge() { //calculate age using today's date and birthday return this.calcAge(); } bark() { return console.log("Woof!"); } updateAttendance() { //add a day to the dog's attendance days at the petsitters this._attendance++; } }
I metodi spesso modificano, aggiornano o eliminano i dati. Tuttavia, i metodi non devono necessariamente aggiornare i dati. Ad esempio, il metodo bark() non aggiorna alcun dato perché l’abbaiare non modifica nessuno degli attributi della classe Cane: nome o compleanno.
Il metodo updateAttendance() aggiunge il giorno in cui il Cane ha frequentato il campo di pet-sitting. L’attributo di presenza è importante da tenere sotto controllo per poter ottenere la fatturazione corretta da dare ai proprietari alla fine del mese.
I metodi sono il modo in cui i programmatori promuovono la riusabilità e mantengono le funzionalità incapsulate all’interno di un oggetto. Questa riusabilità è un grande vantaggio durante il debug. Se c’è un errore, c’è un solo posto dove trovarlo e risolverlo, invece di molti.
Il trattino basso in _attendance indica che la variabile è protetta e non dovrebbe essere modificata direttamente.
Il metodo updateAttendance() modifica _attendance.
Quattro principi dell’OOP: ereditarietà
Le classi figlie ereditano dati e comportamenti dalla classe madre
L’ereditarietà consente alle classi di ereditare le caratteristiche di altre classi. In altre parole, le classi madre estendono attributi e comportamenti alle classi figlie. L’ereditarietà favorisce la riusabilità.
Se gli attributi e i comportamenti di base sono definiti in una classe madre, si possono creare classi figlie che estendono la funzionalità della classe madre e aggiungono attributi e comportamenti supplementari.
Ad esempio, i cani da pastore hanno la capacità unica di radunare gli animali. In altre parole, tutti i cani da pastore sono cani, ma non tutti i cani sono cani da pastore. Per rappresentare questa differenza, creiamo una classe figlia HerdingDog dalla classe madre Dog e aggiungiamo il comportamento unico herd().
I vantaggi dell’ereditarietà sono che i programmi possono creare una classe madre generica e poi creare classi figlie più specifiche, a seconda delle necessità. Questo semplifica la programmazione, perché invece di ricreare più volte la struttura della classe Cane, le classi figlie ottengono automaticamente l’accesso alle funzionalità contenute nella loro classe madre.
Nel seguente frammento di codice, la classe figlia HerdingDog eredita il metodo abbaiare dalla classe madre Dog e la classe figlia aggiunge un ulteriore metodo, herd().
//Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0; constructor(namee, birthday) { this.name = name; this.birthday = birthday; } getAge() { //Getter return this.calcAge(); } calcAge() { //calculate age using today's date and birthday return this.calcAge(); } bark() { return console.log("Woof!"); } updateAttendance() { //add a day to the dog's attendance days at the petsitters this._attendance++; } } //Child class HerdingDog, inherits from parent Dog class HerdingDog extends Dog { constructor(name, birthday) { super(name); super(birthday); } herd() { //additional method for HerdingDog child class return console.log("Stay together!") } }
Notate che la classe HerdingDog non ha una copia del metodo bark(), ma eredita il metodo bark() definito nella classe madre Dog.
Quando il codice chiama il metodo fluffy.bark(), il metodo bark() risale la catena delle classi figlie e genitore per trovare dove è definito il metodo bark.
//Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0; constructor(namee, birthday) { this.name = name; this.birthday = birthday; } getAge() { //Getter return this.calcAge(); } calcAge() { //calculate age using today's date and birthday return this.calcAge(); } bark() { return console.log("Woof!"); } updateAttendance() { //add a day to the dog's attendance days at the petsitters this._attendance++; } } //Child class HerdingDog, inherits from parent Dog class HerdingDog extends Dog { constructor(name, birthday) { super(name); super(birthday); } herd() { //additional method for HerdingDog child class return console.log("Stay together!") } } //instantiate a new HerdingDog object const fluffy = new HerdingDog("Fluffy", "1/12/2019"); fluffy.bark();
Nota: Le classi madre sono anche chiamate superclassi o classi base. Le classi figlie possono essere, invece, chiamate anche sottoclassi, classi derivate o classi estese.
In JavaScript, l’ereditarietà è nota anche come prototipazione. Un oggetto prototipo è un modello per un altro oggetto che eredita proprietà e comportamenti. Possono esistere più modelli di oggetti prototipo, creando una catena di prototipi.
È lo stesso concetto dell’ereditarietà genitore/figlio.
L’ereditarietà è da genitore a figlio. Nel nostro esempio, tutti e tre i cani possono abbaiare, ma solo Maisel e Fluffy possono pascolare.
Il metodo herd() è definito nella classe HerdingDog, quindi i due oggetti Maisel e Fluffy istanziati dalla classe HerdingDog hanno accesso al metodo herd().
Rufus è un oggetto istanziato dalla classe genitore Dog, quindi Rufus ha accesso solo al metodo bark().
Quattro principi dell’OOP: Incapsulazione
Incapsulamento significa contenere tutte le informazioni importanti all’interno di un oggetto ed esporre all’esterno solo le informazioni selezionate. Gli attributi e i comportamenti sono definiti dal codice all’interno del modello della classe.
Poi, quando un oggetto viene istanziato dalla classe, i dati e i metodi vengono incapsulati in quell’oggetto. L’incapsulamento nasconde l’implementazione del codice software interno di una classe e nasconde i dati interni degli oggetti.
L’incapsulamento richiede la definizione di alcuni campi come privati e altri come pubblici.
- Interfaccia privata/interna: metodi e proprietà accessibili da altri metodi della stessa classe.
- Interfaccia pubblica/esterna: metodi e proprietà accessibili dall’esterno della classe.
Utilizziamo un’automobile come metafora dell’incapsulamento. Le informazioni che l’auto condivide con il mondo esterno, come le frecce per indicare le svolte, sono interfacce pubbliche. Il motore, invece, è nascosto sotto il cofano.
È un’interfaccia privata, interna. Quando si guida un’auto lungo la strada, gli altri automobilisti hanno bisogno di informazioni per prendere decisioni, come ad esempio se si sta svoltando a destra o a sinistra. Tuttavia, esporre dati interni e privati come la temperatura del motore confonderebbe gli altri automobilisti.
L’incapsulamento aggiunge sicurezza. Gli attributi e i metodi possono essere impostati come privati, in modo da non potervi accedere al di fuori della classe. Per ottenere informazioni sui dati di un oggetto, si utilizzano metodi e proprietà pubbliche per accedere ai dati o aggiornarli.
Questo aggiunge un livello di sicurezza dove lo sviluppatore sceglie quali dati possono essere visti su un oggetto, esponendoli attraverso metodi pubblici nella definizione della classe.
All’interno delle classi, la maggior parte dei linguaggi di programmazione prevede sezioni pubbliche, protette e private. La sezione pubblica è la selezione limitata di metodi accessibili dal mondo esterno o da altre classi all’interno del programma. La sezione protetta è accessibile solo alle classi figlie.
Il codice privato è accessibile solo all’interno della classe. Per tornare all’esempio del cane/proprietario, l’incapsulamento è ideale per evitare che i proprietari possano accedere alle informazioni private sui cani degli altri.
Nota: JavaScript ha proprietà e metodi privati e protetti. I campi protetti possiedono il prefisso _; i campi privati hanno # come prefisso. I campi protetti vengono ereditati, mentre quelli privati no.
//Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0; constructor(namee, birthday) { this.name = name; this.birthday = birthday; } getAge() { //Getter return this.calcAge(); } calcAge() { //calculate age using today's date and birthday return this.calcAge(); } bark() { return console.log("Woof!"); } updateAttendance() { //add a day to the dog's attendance days at the petsitters this._attendance++; } } //instantiate a new instance of Dog class, an individual dog named Rufus const rufus = new Dog("Rufus", "2/1/2017"); //use getter method to calculate Rufus' age rufus.getAge();
Consideriamo il metodo getAge() nel nostro esempio di codice, i cui dettagli di calcolo sono nascosti all’interno della classe Dog. L’oggetto rufus utilizza il metodo getAge() per calcolare l’età di Rufus.
Incapsulare e aggiornare i dati: Poiché i metodi possono anche aggiornare i dati di un oggetto, lo sviluppatore controlla quali valori possono essere modificati attraverso i metodi pubblici.
Questo ci permette di nascondere le informazioni importanti che non dovrebbero essere modificate dal phishing e dallo scenario più probabile di altri sviluppatori che modificano erroneamente dati importanti.
L’incapsulamento aggiunge sicurezza al codice e rende più facile la collaborazione con sviluppatori esterni. Quando si programma per condividere informazioni con un’azienda esterna, non si vuole esporre i modelli delle classi o i dati privati perché la propria azienda ne detiene la proprietà intellettuale.
Invece, gli sviluppatori creano metodi pubblici che consentono ad altri sviluppatori di chiamare metodi su un oggetto. Idealmente, questi metodi pubblici sono forniti con la documentazione apposita prevista per gli sviluppatori esterni.
I vantaggi dell’incapsulamento sono riassunti qui di seguito:
- Aggiunge sicurezza: Solo i metodi e gli attributi pubblici sono accessibili dall’esterno.
- Protegge da errori comuni: Solo i campi e i metodi pubblici sono accessibili, in modo che gli sviluppatori non modifichino accidentalmente qualcosa di pericoloso.
- Protegge IP: Il codice è nascosto all’interno di una classe; solamente i metodi pubblici sono accessibili agli sviluppatori esterni
- Supportabile: La maggior parte del codice è soggetta ad aggiornamenti e miglioramenti
- Nasconde la complessità: Nessuno può vedere cosa c’è dietro la tenda dell’oggetto!
Quattro principi dell’OOP: astrazione
L’astrazione è un’estensione dell’incapsulamento che utilizza classi e oggetti, che contengono dati e codice, per nascondere agli utenti i dettagli interni di un programma. Ciò avviene creando un livello di astrazione tra l’utente e il codice sorgente più complesso, che aiuta a proteggere le informazioni sensibili contenute nel codice sorgente.
L’astrazione
- Riduce la complessità e migliora la leggibilità del codice
- Facilita il riutilizzo e l’organizzazione del codice
- L’occultamento dei dati migliora la sicurezza dei dati nascondendo i dettagli sensibili agli utenti
- Aumenta la produttività grazie all’astrazione dei dettagli di basso livello.
L’astrazione può essere spiegata anche con le automobili. Pensate a come un autista gestisce un veicolo utilizzando solo il cruscotto dell’auto.
Il conducente usa il volante, l’acceleratore e i pedali del freno per controllare il veicolo. Il conducente non deve preoccuparsi di come funziona il motore o di quali parti vengono utilizzate per ogni movimento. Si tratta di un’astrazione: sono visibili solo gli aspetti importanti necessari al conducente per utilizzare l’auto.
Allo stesso modo, l’astrazione dei dati consente agli sviluppatori di lavorare con informazioni complesse senza preoccuparsi del loro funzionamento interno. In questo modo, contribuisce a migliorare la qualità e la leggibilità del codice.
L’astrazione svolge anche un’importante funzione di sicurezza. Mostrando solo pezzi di dati selezionati e consentendo l’accesso ai dati solo attraverso le classi e la loro modifica attraverso i metodi, proteggiamo i dati dall’esposizione. Per continuare con l’esempio dell’automobile, non si vorrebbe un serbatoio aperto mentre si guida un’auto.
I vantaggi dell’astrazione sono riassunti di seguito:
- Interfacce utente semplici e di alto livello
- Il codice complesso è nascosto
- Sicurezza
- Manutenzione del software più semplice
- Gli aggiornamenti del codice modificano raramente l’astrazione
Quattro principi dell’OOP: polimorfismo
Polimorfismo significa progettare gli oggetti in modo che condividano i comportamenti. Utilizzando l’ereditarietà, gli oggetti possono sovrascrivere i comportamenti condivisi dei genitori con comportamenti specifici dei figli. Il polimorfismo consente allo stesso metodo di eseguire comportamenti diversi in due modi: overriding e overloading del metodo.
Overriding
Il polimorfismo di runtime utilizza l’overriding dei metodi. Con l’overriding dei metodi, una classe figlia può implementare in modo diverso dalla sua classe madre. Nel nostro esempio del cane, potremmo voler dare a TrackingDog un tipo specifico di abbaio, diverso da quello della classe generica del cane.
Il metodo di overriding può creare un metodo bark() nella classe figlia che sovrascrive il metodo bark() nella classe madre, Dog.
//Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0; constructor(namee, birthday) { this.name = name; this.birthday = birthday; } getAge() { //Getter return this.calcAge(); } calcAge() { //calculate age using today's date and birthday return this.calcAge(); } bark() { return console.log("Woof!"); } updateAttendance() { //add a day to the dog's attendance days at the petsitters this._attendance++; } } //Child class TrackingDog, inherits from parent class TrackingDog extends Dog { constructor(name, birthday) super(name); super(birthday); } track() { //additional method for TrackingDog child class return console.log("Searching...") } bark() { return console.log("Found it!"); } //instantiate a new TrackingDog object const duke = new TrackingDog("Duke", "1/12/2019"); duke.bark(); //returns "Found it!"
Overloading
Il polimorfismo durante la compilazione utilizza l’overloading dei metodi. I metodi o le funzioni possono avere lo stesso nome ma un numero diverso di parametri passati nella chiamata al metodo. A seconda del numero di parametri passati, si possono ottenere risultati diversi.
//Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0; constructor(namee, birthday) { this.name = name; this.birthday = birthday; } getAge() { //Getter return this.calcAge(); } calcAge() { //calculate age using today's date and birthday return this.calcAge(); } bark() { return console.log("Woof!"); } updateAttendance() { //add a day to the dog's attendance days at the petsitters this._attendance++; } updateAttendance(x) { //adds multiple to the dog's attendance days at the petsitters this._attendance = this._attendance + x; } } //instantiate a new instance of Dog class, an individual dog named Rufus const rufus = new Dog("Rufus", "2/1/2017"); rufus.updateAttendance(); //attendance = 1 rufus.updateAttendance(4); // attendance = 5
In questo esempio di codice, se non viene passato alcun parametro nel metodo updateAttendance(), viene aggiunto un giorno al conteggio. Se viene passato un parametro in updateAttendance(4), allora 4 viene passato come parametro x in updateAttendance(x) e quindi 4 giorni vengono aggiunti al conteggio.
I vantaggi del polimorfismo sono:
- Oggetti di tipo diverso possono essere passati attraverso la stessa interfaccia.
- Sovrascrittura dei metodi
- Sovraccarico del metodo
Conclusioni
La programmazione orientata agli oggetti richiede di pensare alla struttura del programma e di pianificare un progetto orientato agli oggetti prima di iniziare la codifica. L’OOP nella programmazione informatica si concentra su come suddividere i requisiti in classi semplici e riutilizzabili che possono essere utilizzate per creare istanze di oggetti. Nel complesso, l’implementazione dell’OOP consente di migliorare le strutture dei dati e la riutilizzabilità, con un conseguente risparmio di tempo nel lungo periodo.
Buon apprendimento!