Lezione 3 · Canale 2 · martedì 29 settembre 2026

Modularizzazione e principi dell'object orientation

Progettazione del Software

Riassunto

La modularizzazione è un principio che aiuta a ottenere qualità del software e prepara il passaggio alla programmazione orientata agli oggetti in Java. Un modulo deve avere un obiettivo chiaro, offrire servizi tramite un'interfaccia definita e nascondere i dettagli della propria implementazione. I principi guida sono alta coesione, basso accoppiamento, interfacciamento esplicito e information hiding. L'approccio object-oriented, a differenza di quello procedurale, modella il dominio con oggetti, e si basa su oggetti, classi, astrazione, ereditarietà, polimorfismo, attributi e metodi.

Concetti chiave

  • Modularizzazione — organizzazione del software in unità distinte, ciascuna con un obiettivo definito, che cooperano tramite servizi e relazioni esplicite.
  • Modulo — unità software con un obiettivo descrivibile, che offre servizi agli altri moduli e può a sua volta richiedere servizi.
  • Servizio — operazione che un modulo mette a disposizione degli altri moduli.
  • Interfaccia — insieme dei servizi offerti da un modulo e delle informazioni necessarie per usarli.
  • Client — modulo che richiede un servizio a un altro modulo.
  • Servant o server — modulo software che offre servizi. I due termini sono usati come sinonimi; server può anche indicare una macchina fisica che ospita il software.
  • Unitarietà o alta coesione — principio per cui un modulo corrisponde a un'unità concettuale ben definita e contiene tutti e soli gli aspetti relativi a essa.
  • Basso accoppiamento — principio per cui un modulo dipende dal minor numero ragionevole di altri moduli e limita le informazioni scambiate a quelle necessarie.
  • Interfacciamento esplicito — modo di interagire con un modulo attraverso operazioni e parametri chiaramente dichiarati, evitando dipendenze nascoste come quelle create da variabili globali.
  • Information hiding — principio per cui il modulo espone ciò che gli altri devono sapere per usare i suoi servizi e mantiene nascosti i dettagli interni dell'implementazione.
  • Object orientation — approccio che modella il dominio tramite oggetti, legando dati e operazioni e facendo progredire la computazione attraverso messaggi tra oggetti.
  • Astrazione — processo mentale che individua caratteristiche comuni, trascurando i dettagli specifici, per definire categorie utili alla progettazione.
  • Classe — astrazione che descrive le caratteristiche e le operazioni comuni a una categoria di oggetti.
  • Oggetto — entità concreta che contiene dati e supporta operazioni; è un'istanza di una classe.
  • Ereditarietà — meccanismo che consente a una sottoclasse di riutilizzare dati e operazioni definiti in una classe più generale, aggiungendo le proprie caratteristiche specifiche.
  • Polimorfismo — possibilità di invocare la stessa operazione su oggetti diversi, ottenendo da ciascuno il comportamento specifico previsto.
  • Attributo — caratteristica o dato associato agli oggetti di una classe.
  • Metodo — operazione associata a una classe, invocabile sugli oggetti di quella classe.
  • Istanza — oggetto concreto appartenente a una classe.

Sviluppo

Perché modularizzare

La modularizzazione è un metodo che permette di sostenere le qualità del software. Invece di concentrare tutto in un unico blocco, per esempio un main di migliaia di righe, il software viene organizzato in unità più piccole chiamate moduli.

La diffusione di questo principio nell'industria del software è legata anche all'evoluzione dei linguaggi di programmazione. Il linguaggio C consente di scrivere molto codice in un unico main e non impone di modularizzarlo. I linguaggi orientati agli oggetti, tra cui Java, inducono invece a organizzare il codice in moduli. La tecnologia può quindi favorire determinati metodi di progettazione.

Obiettivo, servizi e interfaccia di un modulo

Un modulo deve avere un obiettivo chiaro e descrivibile: un criterio comune è esprimere il ruolo di un modulo con una frase del tipo "il modulo fa X". In grandi progetti, può essere prevista la verifica da parte di un architetto del software, che controlla se i moduli rispettano le qualità richieste e se saranno mantenibili.

I moduli hanno relazioni strutturali con altri moduli e offrono loro servizi, cioè operazioni elementari. L'insieme dei servizi offerti costituisce l'interfaccia. Il modulo che offre i servizi svolge il ruolo di servant o server; chi li richiede è il client.

Un modulo può essere sia client sia servant, a seconda della relazione. Non è assegnato un ruolo fisso, ma la relazione tra moduli è una rete dinamica di richieste e risposte. Un'architettura comune è quella uno-a-molti, in cui un servant offre lo stesso servizio a molti client. Per comprendere la computazione—il calcolo che porta al risultato—uno sviluppatore deve saper seguire mentalmente la catena di chiamate e risposte tra i moduli.

Rappresentazione del modulo e information hiding

Nei diagrammi il modulo è spesso rappresentato con un rettangolo; i servizi offerti possono essere indicati con il simbolo chiamato lollipop, formato da un'asta e un cerchio. L'interfaccia rappresenta i servizi disponibili, e i client possono essere collegati al servizio specifico che utilizzano.

L'implementazione interna del modulo, cioè il modo in cui realizza i servizi, non deve necessariamente essere visibile ai client. Il client deve sapere che cosa il modulo sa fare, non come lo fa. Così il modulo può migliorare o cambiare la propria implementazione mantenendo la stessa interfaccia. Questo limita gli effetti domino durante la manutenzione: correggere un modulo non richiede di modificare tutti quelli che lo usano.

Il software modulare è paragonabile a un organismo in cui ogni parte ha una funzione riconoscibile e ben definita. In un sistema ben progettato, gli interventi di manutenzione rimangono circoscritti al modulo interessato, analogamente a una cura medica mirata su un organo specifico.

Unitarietà e alta coesione

Il primo principio è l'unitarietà, chiamata anche incapsulamento o alta coesione nel lessico della progettazione. Un modulo deve corrispondere a un'unità concettuale ben definita e contenere tutti e soli gli aspetti che le appartengono.

Per esempio, nella gestione bancaria si possono separare le operazioni tipiche di un conto corrente da quelle relative a un cliente. Non conviene riunire nello stesso modulo concetti che non formano un'unità: non si sommano pere e mele.

Basso accoppiamento e comunicazione esplicita

Un modulo dovrebbe comunicare con il minor numero ragionevole di altri moduli: non deve essere isolato, ma neppure dipendere da un'architettura in cui tutti parlano con tutti. La comunicazione ha un costo in risorse, come memoria, tempo di CPU ed effort di sviluppo.

Il modulo deve esporre le informazioni necessarie agli altri e mantenere nascosti i dettagli interni. L'interfaccia deve specificare chiaramente i servizi offerti e quelli richiesti. Un modulo che ordina dati dovrebbe offrire l'operazione di ordinamento senza obbligare il client a sapere se internamente usa bubble sort, merge sort o un altro algoritmo. In C, i file .h possono specificare le operazioni mentre i file .c ne contengono l'implementazione.

Le variabili globali violano l'information hiding e l'interfacciamento esplicito: più funzioni possono modificarle e generare side effect e dipendenze difficili da individuare.

Esempi dei principi nella pratica

Uno sportello postale che si occupa sia dei bollettini sia del ritiro delle pensioni è un esempio di bassa coesione. Accorpare servizi può ridurre il personale necessario, ma un singolo sportello che fa cose diverse è meno coeso. La scelta progettuale può dipendere dal compromesso tra modularità ed efficienza: per un software in cui le prestazioni sono prioritarie, come quello di controllo di un treno, potrebbe essere necessario dare maggior peso all'efficienza.

Un esempio di forte accoppiamento è dover acquistare una marca da bollo dal tabaccaio per completare una pratica all'anagrafe. Se il tabaccaio è chiuso, la dipendenza crea ritardi. Un'istituzione potrebbe offrire direttamente quel servizio, anche se nel mondo reale intervengono vincoli normativi e fiscali che il software può non avere.

Cartelli chiari che indicano i servizi di uno sportello sono un esempio di interfacciamento esplicito. La carta di credito e la SIM illustrano l'information hiding: l'utente non deve conoscere come sono memorizzati i dati al loro interno, ma solo come usare il servizio. Se l'interfaccia tra telefono e SIM resta stabile, la tecnologia interna può cambiare—per esempio passando dalla SIM fisica alla eSIM—senza alterare l'esperienza dell'utente.

Esempio di cattiva e buona modularizzazione

Un esempio schematico è la ricerca in una lista collegata. Nella versione poco modulare, il client passa la lista alla funzione di ricerca ma comunica l'elemento da cercare attraverso una variabile globale. L'interfacciamento è implicito: chi usa la funzione deve sapere di dover prima impostare quella variabile. Inoltre, la funzione non gestisce una lista vuota e presume che il client se ne sia già occupato.

Queste scelte aumentano le dipendenze, rendono la funzione meno robusta e costringono a documentare dettagli d'uso che dovrebbero essere chiari dall'interfaccia. Il client non dovrebbe dover conoscere i dettagli dell'algoritmo né prevedere ogni condizione interna della libreria.

Nella versione migliore, la funzione riceve esplicitamente la lista e l'elemento da cercare, per esempio tramite una chiamata come cerca(lista, x), e gestisce al proprio interno il caso di lista vuota. Restituisce un risultato booleano; il client si limita a chiamare la funzione e a mostrare il risultato. L'interfaccia diventa più chiara e il codice più facile da usare e mantenere. Questo principio si estende al lavoro di front-end: se i servizi lato server sono ben progettati, chi realizza l'interfaccia grafica può concentrarsi sulla presentazione e chiamare le funzioni disponibili.

Dall'approccio procedurale all'object orientation

Nell'approccio procedurale si parte dalle procedure o funzioni, concentrandosi su come svolgere le operazioni; le strutture dati servono poi a realizzarle. Si distingue tra la procedura, che non restituisce un valore, e la funzione, che lo restituisce; in C sono esempi i tipi void e int di ritorno.

Nell'object orientation il punto di partenza è diverso: si modella un ecosistema di oggetti che rappresenta i concetti del dominio. In un'applicazione bancaria si possono individuare cliente, conto corrente, trasferimento e deposito. L'astrazione aiuta a rappresentare questi concetti nel programma, senza partire subito dall'algoritmo che esegue ogni operazione. I gestionali possono essere paragonati a mondi virtuali che rispecchiano il mondo reale: un e-commerce può avere beni, checkout e carrello della spesa.

L'approccio procedurale può risultare più adatto a software fortemente algoritmici e vincolati dalle prestazioni, come il calcolo fisico o i sistemi di controllo critici. L'ingegnere del software deve conoscere entrambi gli approcci e scegliere in base al problema. La diffusione dell'object orientation è legata alla necessità, soprattutto dall'inizio degli anni 2000, di digitalizzare molti processi gestionali.

Oggetti, classi e astrazione

Nella programmazione a oggetti il programma è composto da moduli che corrispondono alle classi; gli oggetti collaborano scambiandosi messaggi e richieste. L'oggetto è un'entità che contiene dati e supporta operazioni.

Nei linguaggi procedurali come C, il typedef consente di raggruppare informazioni sotto un nome, ma non collega direttamente a quel tipo le operazioni utilizzabili. Nella programmazione a oggetti, dati e operazioni sono associati alla classe: il compilatore può segnalare l'uso di operazioni non supportate dall'oggetto.

La classe è l'astrazione che raggruppa oggetti simili. Persone diverse condividono caratteristiche come nome, cognome o indirizzo, pur avendo valori specifici differenti. La classe rappresenta l'idea comune; l'oggetto è la sua realizzazione concreta. Progettare richiede quindi di individuare correttamente ciò che è comune senza confondere oggetti distinti.

Ereditarietà

L'ereditarietà permette di collocare caratteristiche e operazioni comuni in una classe più generale, così che le classi specifiche possano riutilizzarle. Per esempio, studenti e insegnanti sono persone: dati come nome, cognome e residenza possono essere definiti nella classe generale Persona, mentre ciascuna sottoclasse aggiunge le proprie caratteristiche specifiche. Il riuso evita di riscrivere lo stesso codice.

Le classi possono essere organizzate in gerarchie rappresentabili come alberi, con una radice e nodi figli. Questa struttura facilita la progettazione e la manutenzione del codice.

Polimorfismo

Con il polimorfismo, oggetti diversi possono ricevere la stessa richiesta e rispondere secondo il proprio comportamento specifico. Per esempio, l'operazione insegna è svolta in modo differente da oggetti diversi. Il codice può quindi formulare la stessa richiesta senza dover descrivere ogni volta l'implementazione specifica del singolo oggetto.

Attributi, metodi e istanze

Una classe è caratterizzata da attributi e metodi. Gli attributi descrivono le caratteristiche e i dati degli oggetti, come nome, cognome e indirizzo per una persona. I metodi descrivono le operazioni o i servizi associati alla classe, come insegna per una persona che insegna o studia per uno studente.

Un metodo ha una segnatura, composta dal tipo restituito e dal numero e tipo dei parametri. A differenza di una funzione procedurale generica, è associato a una classe e si invoca su un oggetto di quella classe.

Un oggetto è un'istanza concreta di una classe. In memoria corrisponde a una porzione organizzata secondo la struttura della classe: per una classe con nome, cognome e indirizzo, l'oggetto deve contenere i dati corrispondenti. Le classi sono strumenti di progettazione; nell'elaboratore esistono gli oggetti che ne seguono la struttura. La struttura fissa degli oggetti evita molti errori di runtime, impedendo di accedere a porzioni arbitrarie di memoria come in linguaggi con aritmetica dei puntatori.

Riepilogo e avvio di Java

Si riepiloga il concetto di oggetti, classi, attributi e metodi, le gerarchie di ereditarietà e il polimorfismo. Tra gli argomenti successivi vengono anticipati oggetti complessi, come una lista di oggetti, la concorrenza e il multithreading in Java, e ulteriori aspetti di information hiding.

Java è presentato come linguaggio object-oriented con una vasta base installata e sarà il linguaggio usato nel corso. Molte parole chiave e costrutti sintattici sono già familiari dal C e da Python; gli sviluppi successivi si concentreranno sugli aspetti specifici di Java. Come linguaggio intermedio nella storia della programmazione si può citare il C++, derivato del C a metà degli anni Ottanta.