Sommario
< Home
Stampa

Oggetti e classi

Paradigma ad oggetti 

La programmazione ad oggetti consiste nello sviluppare applicazioni tramite la realizzazione di componenti detti “oggetti”, ovvero elementi software che incapsulano in un’unica entità sia i dati che le funzioni che li elaborano.

Ad esempio, se vogliamo scrivere una applicazione che gestisce l’anagrafica di una persona, una soluzione ad oggetti consiste nel creare un componente “persona” che contiene sia i dati sia le funzioni per gestire la loro gestione, modifica e rappresentazione.

Il software è quindi suddiviso in una opportuna struttura di componenti autonomi – detti appunto “oggetti” – che internamente contengono sia i dati che le funzioni per elaborarli ed esternamente offrono (agli oggetti che li usano) funzioni e proprietà per accedere a questi dati.

Come collaborano gli oggetti? Alcuni oggetti sono creati per utilizzarne altri, altri contengono solo dati e le funzioni strettamente necessarie per leggerli, altri oggetti ancora servono per permettere una migliore organizzazione e comunicazione tra gruppi di oggetti. Nelle applicazioni più grandi gli oggetti sono organizzati in moduli, e i moduli in layer, in modo da creare delle strutture articolate che organizzano i componenti interni e definiscono macro responsabilità all’interno della applicazione.

La programmazione ad oggetti funziona meglio se gli oggetti non si comportano solo come delle “scatole” dove chiunque all’esterno può modificarli, ovvero modificarne le proprietà interne. Questo, infatti, renderebbe difficoltoso garantire il funzionamento di un programma complesso se ogni componente ne potesse liberamente modificare un altro, producendo errori difficili da trovare. E’ necessario un meccanismo, chiamato incapsulamento, che permette al progettista di avere una parte “privata” nell’oggetto, non accessibile dall’esterno, ed una parte “pubblica” di accesso. Ad esempio, sarebbe opportuno che il nostro oggetto “persona” possa mostrare le proprietà “nome” e “cognome” ma che queste non siano modificabili direttamente. Molto meglio una funzione pubblica che cambia contestualmente queste proprietà, e che costituisce un punto di accesso e gestione verso l’esterno.

Nella progettazione di oggetti può capitare di realizzare oggetti con caratteristiche simili tra loro. Ad esempio, un oggetto “impiegato” ed un oggetto “operaio” della stessa ditta hanno alcune caratteristiche in comune, come l’anagrafica (nome, cognome, ecc.). È quindi utile mettere a fattor comune (ovvero “fattorizzare”) componenti comuni creando un oggetto “padre” da cui si derivano degli oggetti “figli”, che ereditano le caratteristiche comuni dal padre aggiungendone/modificandone di proprie specifiche. Con questo sistema si riesce a semplificare concettualmente l’applicazione, si scrive molto meno codice, si fanno molti meno errori. Questo concetto prende il nome di ereditarietà.

Esiste una terza proprietà della programmazione ad oggetti, che si chiama polimorfismo: la capacità cioè di un oggetto di usarne un altro senza necessariamente conoscere cosa fa esattamente, tramite una interfaccia esterna già conosciuta. Questo meccanismo, chiamato anche stereotipizzazione, permette di generalizzare la programmazione e far lavorare insieme oggetti scritti da persone diverse in momenti diversi.

In molti linguaggi la programmazione ad oggetti è collegata al concetto di classe, ovvero di una struttura che permette di definire una specie di modello da cui poi creare gli oggetti veri e propri (le istanze).

Oggetti in Javascript

Anche Javascript supporta la programmazione ad oggetti[1]. Tuttavia a differenza di altri linguaggi la programmazione ad oggetti non richiede necessariamente l’utilizzo di classi. 

Creare oggetti

Ci sono diversi modi per creare un oggetto in Javascript. Partiamo dalla classe Object, una classe che indica un oggetto base.

Forma n.1

const person = new Object(); 
// in alternativa: const person = Object.create();
person.nome = “Mario”;
person.cognome = “Rossi”;
person.saluta = () => {
  console.log(“Hello”);
}

Forma n.2

const person = {
  nome: “Mario”,
  cognome: “Rossi”,
  saluta: () => {
    console.log(“Hello”);
  }
}

In questi esempi, equivalenti tra loro, definiamo delle variabili collegate all’oggetto (dette proprietà) e delle funzioni sempre associate all’oggetto (dette metodi). Si ricorda che gli oggetti una volta creati (in qualsiasi modo dei cinque indicati), possono sempre estesi aggiungendo nuove proprietà e nuovi metodi con qualunque dei metodi (quindi sia con il . che con il dizionario e l’operatore spread.

Questo sistema va bene se abbiamo bisogno di incapsulare dati e funzioni insieme in un oggetto singolo. Se tuttavia di oggetti ne dobbiamo creare diversi, e vogliamo generalizzare la creazione di oggetti, magari sulla base di parametri, questo metodo diventa inefficiente.

Viene quindi in aiuto la funzione costruttore.

Forma n.3

function Person(nome, cognome) => {
  this.nome = nome;
  this.cognome = cognome;
  this.saluta = () = {
    console.log(“Hello”);
  }
}

const person1 = new Person(“Mario”, “Rossi”);
const person2 = new Person(“Maria”, “Bianchi”);

La parola chiave “new” garantisce che venga creato un nuovo oggetto (detto istanza) e su questo oggetto sia applicato il costruttore. La parola chiave this identifica in questo caso l’oggetto appena creato (e quindi non window). Notare che per convenzione le funzioni costruttore hanno l’iniziale maiuscola per distinguerle dalle altre.

In alternativa si possono usare le classi.

Forma n.4

class Person {
  constructor(nome, cognome) {
    this.nome = nome;
    this.cognoe = cognome;
  }

    saluta() {
      console.log(“Hello”);
    }
}

const person = new Person(“Mario”, “Rossi”);

La funzione costruttore (forma n.3) o l’utilizzo della struttura classe (forma n.4) sono completamente interscambiabili in Javascript, e vengono usate nello stesso identico modo.

Forma n.5

Per ottenere incapsulamento però nessuna delle forme precedenti va bene. Gli oggetti sono tutti con attributi pubblici quindi scrivibili e quindi soggetti ad errori.

Per risolvere questo problema possiamo usare la funzione generatrice che in questa accezione non restituisce una funzione ma un oggetto. Javascript permette di creare oggetti con proprietà private attraverso il concetto di closureUna closure è una funzione generatrice che nell’esempio visto crea delle variabili interne e poi restituisce una funzione che le usa. In questo caso faremo restituire un oggetto.

function createPerson(nome, cognome) => {
  const _nome = nome;
  const _cognome = cognome;
  const _saluta = () => console.log(`Ciao sono ${_nome} ${_cognome}`);

      return {
            getNome: () => _nome,
            getCOgnome: () => _cognome,
            saluta: _saluta
      }
}

const person = createPerson(“Mario”, “Rossi”);
person.saluta();

Ereditarietà

L’ereditarietà in Javascript può essere implementata sia nelle classi usando la parola chiave extends che direttamente a livello di oggetto, con prototype.

Extends utilizza un meccanismo con sintassi simil-Java nelle classi.

Ad esempio possiamo estendere la classe Person così:

class Employee extends Person {
  constructor(nome, cognome, matricola) {
    super(nome, cognome);
    this.matricola = matricola;
  }

  info() {
    console.log(this.matricola + "- ")
  }  
}

Il costruttore dell’oggetto figlio può richiamare il costruttore del padre usando la parola chiave super.  È possibile sovrascrivere i metodi del padre, tramite un meccanismo che si chiama override. Il metodo che esegue l’override si chiamerà nello stesso modo. Inoltre è sempre possibile chiamare i metodi del padre dal figlio con la parola chiave “super”, anche dai metodi che fanno ovveride.

Prototype è un meccanismo di Javascript che permette di associare un oggetto con le funzioni di “padre” ad un oggetto. Tutti gli oggetti Javascript hanno infatti una proprietà “prototype” che può essere valorizzata con l’oggetto padre. Se usato con le classi (o le funzioni costruttore) allora anche tutti gli oggetti ereditano la funzione.

class Person {
  constructor(nome, cognome) {
    this.nome = nome;
    this.cognome = cognome;
}

   saluta() {
     console.log(“Hello”);
   }
}

const p1 = new Person(“Mario”, “Rossi”);
const p2 = new Person(“Sara”, “Verdi”);

Person.prototype.nationality = “italiana”;
console.log(p1.nationality); --> “italiana”
console.log(p2.nationality); --> “italiana”

In questo esempio si può notare che l’aggiunta del prototype aggiunge funzionalità a Person, e quindi non solo ai futuri oggetti che saranno creati con Persona ma pure agli oggetti già creati. Quindi se si modifica il prototipo di una classe, questo ha un impatto su tutti gli oggetti creati con quella classe, compresi quelli già esistenti. Questo meccanismo consente di mutare il comportamento di tutte le istanze di una classe, anche quelle già create.

La grande differenza tra i due tipi di ereditarietà è che l’ereditarità mediante extend è una ereditarietà a compile time, ovvero quando il programmatore progetta l’applicazione deve tenere conto dell’intera gerarchia di classi, e di tutte le situazioni possibili, anche se magari poi in esecuzione non sempre serve avere tutte le funzioni di tutta la gerarchia. Con l’eredità basata su prototipi questa è invece a runtime, e può essere aggiunta solo se e quando serve. Questo significa che usare questo tipo di ereditarietà produce un software che occupa molta meno memoria ed è in generale più semplice in esecuzione, anche se questo comporta dei rischi se non si ha chiaro cosa si sta facendo e quali sono gli oggetti già creati.


[1] Javascript non supporta solo la programmazione ad oggetti, ma anche i paradigmi di programmazione imperativo (come il C) e funzionale.

[2] Le closure possono essere usate anche per creare funzioni (cioè sono funzioni che restituiscono funzioni) dette anche funzioni di ordine superiore, e sono alla base della programmazione funzionale, una tecnica di programmazione alternativa a quella ad oggetti, che si rivela molto utile nel processamento dei dati, nella gestione di eventi, e nella presentazione di informazione, che sono poi proprio le attività tipiche di Javascript.

[3] In futuro anche con le classi dovrebbe essere possibile dichiarare membri privati anteponendo prima del nome della proprietà il simbolo #, ma ancora questa modifica non è stata approvata.