Orale_Ingegneria_del_Sofware.pdf
Document Details
Uploaded by AltruisticLogarithm
Univpm
Full Transcript
Università Politecnica delle Marche Facoltà di Ingegneria Corso di Laurea in Ingegneria Informatica e dell’Automazione Preparazione Orale in Ingegneria del Software Ghitarrari Andrea Indice 1 I Pattern...
Università Politecnica delle Marche Facoltà di Ingegneria Corso di Laurea in Ingegneria Informatica e dell’Automazione Preparazione Orale in Ingegneria del Software Ghitarrari Andrea Indice 1 I Pattern 4 1.1 Introduzione......................................... 4 1.2 I Pattern Architetturali.................................. 4 1.2.1 Il Pattern Model-View-Controller......................... 5 1.2.2 Il Pattern Layered Arichitecture......................... 6 1.2.3 Il Pattern DAO (Data Access Object)...................... 7 1.2.4 Il Pattern Repository............................... 8 1.2.5 Il Pattern Client-Server.............................. 8 1.2.6 Il Pattern Pipe-and-Filter............................. 10 1.3 I Design Pattern...................................... 11 1.4 Design Pattern - I Pattern Creazionali.......................... 13 1.4.1 Il Pattern Factory Method............................ 13 1.4.2 Il Pattern Abstract Factory............................ 14 1.4.3 Il Pattern Builder................................. 14 1.4.4 Il Pattern Prototype................................ 15 1.4.5 Il Pattern Singleton................................ 16 1.5 Design Pattern - I Pattern Strutturali.......................... 17 1.5.1 Il Pattern Adapter................................. 17 1.5.2 Il Pattern Bridge.................................. 17 1.5.3 Il Pattern Composite................................ 18 1.5.4 Il pattern Decorator................................ 19 1.5.5 Il Pattern Facade.................................. 20 1.5.6 Il Pattern Flyweight................................ 20 1.5.7 Il Pattern Proxy.................................. 21 1.6 Design Pattern - I Pattern Comportamentali...................... 22 1.6.1 Il Pattern Chain of Responsability........................ 22 1.6.2 Il Pattern Command................................ 23 1.6.3 Il Pattern Iterator................................. 23 1.6.4 Il Pattern Mediator................................ 24 1.6.5 Il Pattern Memento................................ 25 1.6.6 Il Pattern Observer................................. 26 1.6.7 Il Pattern State................................... 26 1.6.8 Il Pattern Strategy................................. 27 1.6.9 Il Pattern Template Method............................ 28 1.6.10 Il Pattern Visitor.................................. 29 2 Ciclo di Vita di un Progetto 31 2.1 I Modelli Tradizionali................................... 31 2.1.1 Il Ciclo di Vita a Cascata............................. 31 2.1.2 Il Ciclo di Vita a Cascata con Backtrack..................... 33 2.1.3 Il Ciclo di Vita Iterativo.............................. 33 2.1.4 Il Ciclo di Vita a Spirale.............................. 33 2.2 Le Metodologie Agili.................................... 35 2.2.1 eXtreme Programming (XP)........................... 35 1 2.2.2 Scrum........................................ 36 2.2.3 Kanban....................................... 38 2.2.4 Scrumban...................................... 42 2.2.5 DevOps....................................... 43 3 Il Software Come Prodotto 45 3.1 Project Management.................................... 45 3.1.1 Stima dei tempi di un progetto.......................... 45 3.1.2 Riutilizzo del software............................... 45 3.1.3 Gestione delle persone............................... 45 3.1.4 Lavoro di squadra................................. 46 3.1.5 Leadership..................................... 46 3.1.6 Controllo della qualità............................... 46 3.2 Il Software Come Prodotto................................ 46 3.2.1 Rapporti tra Committente e Sviluppatore.................... 46 3.2.2 Il Mese-Uomo.................................... 47 3.2.3 La pianificazione delle risorse........................... 48 3.2.4 Le Metriche del Software............................. 49 3.2.5 Configuration Management............................ 50 3.2.6 Function Point Analysis.............................. 50 4 Il Software Come Processo 53 4.1 Verifica, Validazione e Test................................ 53 4.1.1 Revisione di Qualità................................ 53 4.1.2 Verifica....................................... 53 4.1.3 Validazione e Usabilità............................... 53 4.1.4 Testing....................................... 54 4.2 Manutenzione e Evoluzione del Software......................... 55 4.2.1 Quattro Categorie di Manutenzione....................... 55 4.2.2 Falsi Miti sulla Manutenzione del Software................... 55 4.2.3 Costi e Terminologia della Manutenzione.................... 56 4.2.4 Reality Check: la Manutenzione del Software Oggi............... 56 4.2.5 Le Leggi di Lehman: l’Evoluzione del Software................. 57 4.3 Configuration Management................................ 57 4.4 Qualità del Software.................................... 58 4.4.1 Modello di Qualità Interna ed Esterna...................... 59 4.4.2 Modello di Qualità in Uso............................. 60 4.5 Il Riuso del Software.................................... 60 4.5.1 Riuso degli Application Framework........................ 61 4.5.2 Linee di Prodotto Software............................ 61 4.5.3 Riuso di Application System........................... 61 4.5.4 Sistemi Applicativi Integrati........................... 61 5 Ingegneria Del Software Avanzata 62 5.1 Component-Based Development Paradigm........................ 62 5.2 Distributed Computing.................................. 63 5.2.1 Architetture di calcolo distribuito......................... 64 2 5.2.2 Differenza tra calcolo parallelo e distribuito................... 64 5.3 Service Oriented Architecture............................... 65 5.3.1 Principi della SOA................................. 65 5.3.2 Funzionamento della SOA............................. 65 5.4 Real-Time Computing................................... 66 5.4.1 Criteri per la Classificazione dei Sistemi Real-Time............... 66 5.4.2 High-Performance Computing........................... 66 5.4.3 Near Real-Time................................... 67 5.5 Cloud Computing...................................... 67 5.5.1 Tipi di Servizi Cloud................................ 67 5.5.2 Usi del Cloud Computing............................. 68 5.6 L’Architettura a Microservizi............................... 68 5.6.1 Vantaggi dell’architettura a microservizi:..................... 68 5.6.2 Svantaggi dell’architettura a microservizi:.................... 69 5.6.3 Scaling nell’architettura a microservizi...................... 69 5.6.4 Modello di sviluppo dei microservizi....................... 69 5.6.5 Sicurezza nell’architettura a microservizi..................... 70 5.6.6 Pattern di composizione dei microservizi..................... 70 5.7 Dependability dei Sistemi Software............................ 70 5.7.1 Proprietà Chiave della Dependability....................... 70 5.7.2 Raggiungere la Dependability........................... 71 5.7.3 Costi della Dependability............................. 71 5.7.4 Sistemi Socio-Tecnici................................ 72 5.7.5 Proprietà Emergenti................................ 72 5.7.6 Ridondanza e Diversità.............................. 72 5.7.7 Processi Dependable................................ 72 3 1 I Pattern 1.1 Introduzione Progettare applicazioni OOP complesse, riusabili, flessibili ed estensibili non è facile. Per questo motivo gli sviluppatori preferiscono adottare soluzioni che in passato si sono rilevate efficaci per la risoluzione di problemi di progettazione comuni. Queste soluzioni prendono il nome di pattern, attenzione però, i pattern non sono pezzi di codice, ma descrizioni ad alto livello di come un software va implementato, quest’ultima parte spetta al programmatore. Ci sono diversi tipi di pattern suddivisi per categorie, dette cluster, in base al loro livello di astrazione. Pattern Architetturali : descrivono lo schema organizzativo della struttura del sistema soft- ware, definendo le relazioni tra i suoi componenti principali (ad esempio il pattern MVC ). Design Pattern: si focalizzano sulla progettazione a livello di classi e oggetti, fornendo soluzioni a problemi comuni nella programmazione orientata agli oggetti. I design pattern più famosi sono i 23 pattern GoF (Gang of Four) e si dividono in tre categorie: – Creazionali; – Strutturali; – Comportamentali. Pattern Idiomatici : offrono soluzioni specifiche per un particolare linguaggio di program- mazione o teconlogia, aiutandoci a risolvere dei problemi sfruttando le peculiarità della pi- attaforma che stiamo usando. 1.2 I Pattern Architetturali I pattern architetturali sono utilizzati per descrivere lo schema organizzativo generale di un sistema software, definendo le relazioni tra i suoi componenti principali. Operano ad un livello di astrazione più ampio rispetto ai design pattern, fornendo una visione generale del sistema. I pattern architetturali sono cruciali nella progettazione di sistemi software perché: Forniscono una visione di alto livello: consentono di comprendere l’organizzazione generale del sistema e le interazioni tra i suoi componenti principali. Promuovono il riutilizzo e la manutenibilità: l’utilizzo di pattern architetturali noti facilita il riutilizzo di soluzioni già sperimentate e rende il sistema più facile da manutenere. Favoriscono la flessibilità e l’estensibilità: i pattern architetturali aiutano a creare sistemi che possono essere facilmente modificati ed estesi nel tempo. I pattern architetturali piu famosi sono: Model-View-Controller; Layered Architecture; DAO (Data Access Object); 4 Repository; Client-Server; Pipe-and-Filter. 1.2.1 Il Pattern Model-View-Controller La funzione principale del pattern MVC è quella di separare la gestione dei dati, che viene svolta dal Model, la presentazione all’utente, curata dalle View, e l’interazione con l’utente, controllata appunto dai Controller. Model: Si occupa di rappresentare i dati e la logica di business a loro associata. È respon- sabile della gestione dei dati, del loro accesso e della loro modifica, e di notificare eventuali cambiamenti agli altri componenti. Il model implementa le regole e i processi che governano l’applicazione. Ad esempio in un e-commerce l’applicazione di sconti o la gestione di un carrello. È responsabile inoltre della gestione e del check dei dati prima che questi vengano elaborati e memorizzati. Fornisce anche metodi specifici che permettono agli altri comenenti di interagire con i dati e la logica di business in modo controllato. View: si occupa di creare l’interfaccia per l’utente, e di aggiornarla in base ai dati forniti dal model. Deve essere sempre in attesa di ricevere notifiche da parte del model quando dei dati vengono cambiati, questo sistema di notifica puo avvenire in vari modi, uno di questi è l’utilizzo del pattern Observer. Anche se la View non modifica direttamente i dati, questa potrebbe interrogare il Model per ottenere informazioni necessarie sullo stati dei dati da visualizzare, ad esempio il valore corrente di un certo attributo di un oggetto. La View infine ha il compito di passare gli input ricevuto dall’utente al Controller per la loro gestione. Controller: riceve gli input dell’utente provenienti dalla View, li interpreta, per capire l’azione che l’utente vuole fare, alle volte questo però è complicato perché il Controller potrebbe dover interpretare l’azione in base allo stato corrente dell’applicazione o del Model. Se l’azione intercettata dal Controller richiede una modifica dei dati, questo deve interagire con il Model tramite metodi specifici. In alcuni casi il Controller potrebbe dover aggiornare direttamente la View, ad esempio quando i cambiamenti sono puramente esteitici e non hanno ripercussioni sui dati del Model. Infine il Controller può essere responsabile della gestione degli errori che si verificano durante l’elaborazione dell’input del’utente, o durante l’interazione con il Model. Le classi nel pattern Model-View-Controller (MVC) comunicano tra loro in modo unidirezionale e disaccoppiato per mantenere la separazione delle responsabilità e promuovere la modularità. Flusso di Comunicazione: 1. Input dell’utente: L’interazione ha inizio con un input dell’utente sulla View, ad esempio un click su un pulsante o l’inserimento di testo in un campo di testo. La View riceve questo input ma non lo elabora direttamente. 2. Notifica al Controller : La View notifica il Controller dell’input ricevuto, senza specificare come gestirlo. La View passa l’input al Controller come un evento generico. 5 3. Il Controller interpreta l’input: Il Controller riceve l’evento dalla View e ne interpreta il significato in base al contesto dell’applicazione. Ad esempio, un click su un pulsante ”Salva” avrà un significato diverso da un click su un pulsante ”Annulla”. 4. Il Controller aggiorna il Model (o la View): In base all’input interpretato, il Controller può: Aggiornare il Model: Se l’input dell’utente richiede una modifica ai dati o alla logica di business, il Controller invoca i metodi appropriati del Model per effettuare le modi- fiche. Ad esempio, il Controller potrebbe chiamare il metodo setTime() del Model per aggiornare l’ora in un’applicazione per la gestione di un orologio. Aggiornare direttamente la View: In alcuni casi, il Controller potrebbe aggiornare diret- tamente la View, principalmente per cambiamenti estetici che non influenzano i dati del Model. Ad esempio, il Controller potrebbe cambiare il colore di un pulsante in risposta a un’azione dell’utente. 5. Il Model notifica la View (se necessario): Dopo che il Model è stato aggiornato dal Controller, notifica la View (o le View) interessate del cambiamento. Questo permette alla View di aggiornarsi e riflettere le modifiche ai dati. La View non richiede esplicitamente informazioni al Model, ma si limita ad ascoltare le notifiche di aggiornamento. 6. La View si aggiorna: Ricevuta la notifica dal Model, la View si aggiorna per riflettere le modifiche ai dati. Questo può comportare l’aggiornamento di elementi specifici dell’interfaccia utente, come il testo di un campo o il contenuto di una lista. La View si limita a visualizzare i dati, senza conoscere o gestire la logica che ha portato a tali modifiche. Il pattern Model-View-Controller (MVC), offre il vantaggio significativo di separare i dati di un’applicazione dalla loro rappresentazione e dalle interazioni degli utenti. Questo significa che i dati possono essere modificati senza influenzare il modo in cui sono presentati all’utente, e vicev- ersa. Inoltre, MVC supporta la presentazione degli stessi dati in modi differenti, garantendo che le modifiche fatte in una rappresentazione si riflettano in tutte le altre. Tuttavia, questa flessibilità può comportare un aumento della complessità del codice, specialmente quando il modello dei dati e le interazioni utente sono semplici. In questi casi, l’utilizzo di MVC potrebbe richiedere la scrittura di più codice rispetto ad un approccio più semplice, senza offrire benefici significativi 1.2.2 Il Pattern Layered Arichitecture È un modello architetturale che organizza le funzionalità di un sistema in strati separati, in cui ogni strato si basa sulle funzioni e sui servizi offerti dallo strato immediatamente sottostante. Questo tipo di approccio offre diversi vantaggi: sviluppo incrementale: ogni strato può essere sviluppato e testato indipendentemente dagli altri; modificabilità e portabilità: la modifica o la sostituzione di uno strto non influisce sugli altri strati, a condizione che l’interfaccia tra gli strati rimanga invariata; semplificazione dello sviluppo multipiattaforma: gli strati dipendenti dalla macchina (tipo quelli che interagiscono con un SO o un DB) possono essere reimplementati per adattare l’applicazione a diverse piattaforme, senza dover modificare tutto. 6 Tuttavia, ci sono anche alcuni svantaggi da considerare: difficoltà di separazione netta: nella pratica, può essere difficile mantenere una separazione netta tra gli strati. Potrebbe essere necessario che uno strato di livello superiore interagisca direttamente con strati inferiori, violando il principio di stratificazione e aumentando la com- plessità; impatto sulle prestazioni : l’elaborazione di una richiesta attraverso più strati può comportare un overhead in termini di prestazioni, poiché ogni strato deve interpretare la richiesta e inoltrarla allo strato successivo. 1.2.3 Il Pattern DAO (Data Access Object) È un pattern architetturale che ha lo scopo di disaccoppiare l’accesso ai dati dalla loro effettiva memorizzazione. in pratica consente di estrarre la logica di accesso ai dati, rendendo l’applicazione indipendente dal tipo specifico di DBMS utilizzato. L’utilizzo del pattern DAO porta diversi van- taggi: portabilità dell’applicazione: il pattern DAO consente di cambiare il DBMS sottostante senza dover modificare la logica di business dell’applicazione. Ciò è particolarmente utile in fase di sviluppo, quando si potrebbe voler utilizzare un DBMS diverso da quello che sarà utilizzato in produzione, o quando si prevede di dover migrare l’applicazione su un altro DBMS in futuro; semplificazione della manutenzione: la separazione tra la logica di business e quella di accesso ai dati rende l’applicazione più facile da manutenere. Le modifiche al DBMS o alla struttura del database possono essere implementate senza dover modificare la logica di business; miglioramento della testabilità: il pattern DAO semplifica il testing della logica di business dell’applicazione, in quanto è possibile utilizzare degli oggetti mock per simulare l’accesso al database. Il pattern DAO utilizza oggetti DAO, classi che incapsulano la logica di accesso ai dati per una specifica entità o tabella del database. Ciascun oggetto DAO offre un’interfaccia per le operazioni CRUD (Create, Read, Update, Delete) sull’entità, nascondendo al resto dell’applicazione i dettagli dell’implementazione dell’accesso al database. Le fonti forniscono un diagramma delle classi per il pattern DAO, che include i seguenti oggetti: BusinessObject: Rappresenta la classe con la logica di business dell’applicazione. Questa classe non ha la responsabilità di memorizzare i dati, ma solo di specificare cosa e come modificarli. DataAccessObject: Questa classe nasconde la DataSource effettiva e può essere facilmente sostituita con un’altra qualora la sorgente informativa cambiasse. DataSource: Questo oggetto è associato alla sorgente informativa effettiva (es. il database). TransferObject: Utilizzato per trasferire i dati effettivi tra DataAccessObject e BusinessOb- ject. Questo oggetto rappresenta i dati memorizzati nel database e non è direttamente con- nesso a DataSource. Qualsiasi modifica al TransferObject deve passare attraverso il DataAc- cessObject prima di poter diventare permanente. 7 1.2.4 Il Pattern Repository È un pattern architetturale che definisce come una serie di componenti interattivi possono condi- videre i dati. Invece di accedere direttamente al database o a un’altra fonte di dati, i componenti interagiscono con un repository, un intermediario che astrae la logica di accesso ai dati. Tutti i dati di un sistema sono gestiti in un database centrale (detto repository) accessibile da tutti i componenti del sistema. I componenti non interagiscono direttamente tra loro, ma solo attraverso il repository. Il Repository Pattern è particolarmente utile quando: Nel sistema vengono generate grandi quantità di informazioni che devono essere memorizzate a lungo termine. Si ha a che fare con sistemi data-driven, dove l’inserimento di dati nel repository innesca un’azione o l’utilizzo di uno strumento. L’utilizzo del Repository Pattern offre diversi vantaggi: Indipendenza dei componenti : Un componente non ha bisogno di conoscere l’esistenza di altri componenti. Questo riduce l’accoppiamento tra i componenti e rende il sistema più facile da manutenere ed evolvere. Semplicità di notifica: Le modifiche apportate da un componente possono essere propagate facilmente agli altri componenti tramite il repository. Gestione coerente dei dati : Tutti i dati vengono gestiti in modo coerente (ad esempio, i backup vengono eseguiti contemporaneamente) poiché si trovano nello stesso posto. Nonostante i suoi vantaggi, il Repository Pattern presenta anche alcuni svantaggi: Punto unico di errore: Il repository rappresenta un punto unico di errore, nel senso che i suoi problemi influiscono sull’intero sistema. Potenziali inefficienze: Organizzare tutte le comunicazioni attraverso il repository potrebbe introdurre inefficienze, soprattutto in sistemi molto grandi. Difficoltà di integrazione: Potrebbe essere difficile integrare nuovi componenti se i loro modelli di dati non sono conformi allo schema concordato del repository. Complessità di distribuzione: Distribuire il repository su più macchine può essere complesso e richiedere la gestione di più copie dei dati. 1.2.5 Il Pattern Client-Server È un pattern architetturale che descrive un’organizzazione a runtime comune nei sistemi distribuiti. In questo modello, un sistema è composto da due tipi principali di componenti: client e server. In un’architettura client-server, il sistema è presentato come un insieme di servizi, ciascuno dei quali viene fornito da un server separato. I client sono gli utenti di questi servizi e accedono ai server per utilizzarli. Questo pattern è particolarmente adatto quando: È necessario accedere ai dati di un database da più postazioni. 8 Il carico sul sistema è variabile, poiché i server possono essere replicati per gestire la richiesta. Un sistema Client-Server è composto dai seguenti componenti: Server: Forniscono servizi specifici ai client, come servizi di stampa, gestione dei file o com- pilazione di linguaggi di programmazione. I server sono componenti software e più server possono essere eseguiti sullo stesso computer. Client: Richiedono e utilizzano i servizi offerti dai server. I client sono in genere istanze di programmi eseguiti su computer diversi. Rete: Consente ai client di accedere ai server e ai loro servizi. I sistemi client-server sono spesso implementati come sistemi distribuiti, collegati tramite protocolli Internet. I client accedono ai servizi offerti dai server tramite chiamate di procedura remote. Questo processo utilizza un protocollo di richiesta-risposta (come HTTP), dove: 1. Il client invia una richiesta al server. 2. Il server elabora la richiesta. 3. Il server invia una risposta al client. 4. Il client riceve la risposta ed elabora i dati ricevuti. Il pattern Client-Server offre diversi vantaggi: Architettura distribuita: I server possono essere distribuiti su una rete, consentendo la scala- bilità e la tolleranza ai guasti. Accesso centralizzato ai dati : I dati possono essere memorizzati e gestiti centralmente sui server, facilitando l’amministrazione e la sicurezza. Riuso del codice: Le funzionalità comuni possono essere implementate una sola volta sul server e riutilizzate da più client. Semplicità di manutenzione: I server possono essere aggiornati e modificati senza influire sui client, a condizione che l’interfaccia del servizio rimanga invariata. Indipendenza dei componenti : Client e server sono componenti indipendenti, quindi possono essere sviluppati e mantenuti separatamente. Nonostante i suoi vantaggi, il pattern Client-Server presenta anche alcuni svantaggi: Punto unico di errore: I server rappresentano punti unici di errore. Se un server si guasta, i client che dipendono da quel servizio non saranno in grado di utilizzarlo. Sicurezza: I server, essendo accessibili dalla rete, sono vulnerabili ad attacchi informatici. Prestazioni : Le prestazioni di un sistema client-server dipendono dalla rete e dal carico sui server, che possono variare e diventare imprevedibili. Complessità di gestione: Gestire un sistema distribuito con più server può essere complesso, soprattutto se i server sono di proprietà di diverse organizzazioni. 9 1.2.6 Il Pattern Pipe-and-Filter È un pattern architetturale che descrive l’organizzazione a runtime di un sistema in cui i dati vengono elaborati in modo sequenziale o parallelo da una serie di componenti chiamati filter. I dati fluiscono tra i filtri attraverso canali di comunicazione chiamati pipe. L’elaborazione dei dati del sistema è organizzata in modo che ogni componente di elaborazione (filtro) sia discreto e svolga un particolare tipo di trasformazione dei dati. I dati fluiscono come in un tubo (pipe) da un componente all’altro per essere elaborati. In questo esempio viene mostrato l’utilizzo del pattern Pipe-and-Filter per un sistema che elabora l’emissione di fatture: Questo pattern è particolarmente adatto per: Applicazioni di elaborazione dati batch o basate su transazioni, dove gli input vengono elab- orati in fasi separate per generare gli output. Sistemi integrati, dove l’interazione con l’utente è limitata. Un sistema Pipe-and-Filter è composto dai seguenti componenti: Filtro (Filter): Un componente autonomo che esegue una specifica trasformazione sui dati che riceve in input. I filtri sono indipendenti l’uno dall’altro e non conoscono l’esistenza degli altri filtri nella pipeline. Tubo (Pipe): Un canale di comunicazione che collega due filtri. Le pipe sono responsabili del trasferimento dei dati da un filtro all’altro. Il pattern Pipe-and-Filter opera in questo modo: 1. I dati grezzi vengono immessi nella pipeline. 2. I dati fluiscono attraverso i filtri, ciascuno dei quali esegue la sua specifica trasformazione. 3. I dati elaborati vengono emessi dall’ultimo filtro della pipeline. Le trasformazioni possono essere eseguite: Sequenzialmente: I dati vengono elaborati da un filtro alla volta, nell’ordine in cui sono collegati nella pipeline. In parallelo: Più filtri possono elaborare i dati contemporaneamente. 10 Sequenzialmente I dati vengono elaborati da un filtro alla volta, nell’ordine in cui sono collegati nella pipeline. In parallelo Più filtri possono elaborare i dati contemporaneamente. I dati possono essere elaborati: Elemento per elemento: Ogni filtro elabora un singolo elemento di dati alla volta. In blocco: I filtri elaborano blocchi di dati. Il pattern Pipe-and-Filter offre diversi vantaggi: Semplicità e comprensibilità: Il modello è facile da capire e da implementare. Riuso dei componenti : I filtri, essendo componenti indipendenti, possono essere facilmente riusati in altre pipeline. Flessibilità e manutenibilità: È semplice aggiungere, rimuovere o modificare i filtri senza influenzare gli altri componenti della pipeline. Elaborazione parallela: Il modello si presta bene all’elaborazione parallela dei dati, miglio- rando potenzialmente le prestazioni. Nonostante i suoi vantaggi, il Pipe-and-Filter Pattern presenta anche alcuni svantaggi: Formato dati condiviso: I filtri devono concordare un formato dati comune per lo scambio di informazioni. Questo può limitare la flessibilità nella scelta delle strutture dati e aumentare gli overhead di conversione dei dati. Gestione degli errori : La gestione degli errori può essere complessa, poiché un errore in un filtro può influenzare l’intera pipeline. Interazione limitata con l’utente: Il modello non è adatto per sistemi altamente interattivi, poiché l’elaborazione dei dati è tipicamente lineare e non prevede un’interazione frequente con l’utente. 1.3 I Design Pattern I design pattern sono soluzioni tipiche a problemi comuni nella progettazione del software. Sono come progetti predefiniti che puoi personalizzare per risolvere un problema di progettazione ricor- rente nel tuo codice. Non si tratta di pezzi di codice specifici, ma di concetti generali per risolvere problemi specifici. I design pattern non sono concetti oscuri e sofisticati, ma soluzioni tipiche ai problemi comuni della programmazione orientata agli oggetti. La maggior parte dei pattern sono descritti in modo formale per facilitarne la riproduzione in diversi contesti. Una descrizione tipica di un pattern comprende sezioni come: Scopo: descrizione del problema e della soluzione. Motivazione: spiegazione dettagliata del problema e della soluzione offerta dal pattern. Struttura delle classi: illustrazione di come le diverse parti del pattern sono collegate tra loro. 11 Esempio di codice: esemplificazione dell’idea alla base del pattern. Il cluster GoF (Gang of Four) è un insieme di 23 design pattern descritti nel libro ”Design Patterns: Elements of Reusable Object-Oriented Software” di Erich Gamma, John Vlissides, Ralph Johnson e Richard Helm. Questo libro è diventato rapidamente un best-seller e ha reso popolare l’uso dei design pattern nella programmazione orientata agli oggetti. I pattern GoF sono suddivisi in tre categorie: 1. Pattern Creazionali: Si occupano della creazione di oggetti, fornendo un’astrazione del processo di creazione delle istanze delle classi e favorendo l’indipendenza del sistema dalle modalità di creazione e dai tipi concreti effettivamente generati. Invece di creare oggetti direttamente, si utilizzano questi pattern per creare oggetti in modo più flessibile e controllato. I pattern creazionali GoF sono cinque: Abstract Factory: fornisce un’interfaccia per creare famiglie di oggetti correlati o dipendenti senza specificare le loro classi concrete. Builder: separa la costruzione di un oggetto complesso dalla sua rappresentazione, consentendo di creare rappresentazioni diverse con lo stesso processo di costruzione. Factory Method: definisce un’interfaccia per creare un oggetto, ma lascia che siano le sottoclassi a decidere quale classe istanziare. Prototype: crea nuovi oggetti copiando un oggetto prototipo esistente. Singleton: garantisce che una classe abbia una sola istanza, fornendo un punto di accesso globale a tale istanza. 2. Pattern Strutturali Si occupano dell’organizzazione delle classi e degli oggetti in strutture più grandi e complesse, descrivendo come assemblare classi e oggetti per formare strutture più grandi, mantenendo al contempo flessibilità e efficienza. I pattern strutturali GoF sono sette: Adapter: converte l’interfaccia di una classe in un’altra interfaccia compatibile con il client. Bridge: disaccoppia un’astrazione dalla sua implementazione, consentendo a entrambe di variare in modo indipendente. Composite: compone oggetti in strutture ad albero per rappresentare gerarchie parte- tutto. Decorator: aggiunge dinamicamente nuove responsabilità a un oggetto. Facade: fornisce un’interfaccia semplificata a un sistema complesso. Flyweight: condivide oggetti simili per supportare l’efficienza della memoria. Proxy: fornisce un sostituto per un altro oggetto per controllare l’accesso ad esso. Composite: compone oggetti in strutture ad albero per rappresentare gerarchie parte- tutto. Decorator: aggiunge dinamicamente nuove responsabilità a un oggetto. Facade: fornisce un’interfaccia semplificata a un sistema complesso. Flyweight: condivide oggetti simili per supportare l’efficienza della memoria. 12 Proxy: fornisce un sostituto per un altro oggetto per controllare l’accesso ad esso. 3. Pattern Comportamentali Si occupano della comunicazione e dell’interazione tra classi e oggetti, definendo come gli oggetti interagiscono e si scambiano messaggi in modo flessibile ed estensibile. I pattern comportamentali GoF sono undici: Chain of Responsibility: evita di accoppiare il mittente di una richiesta al suo ricevente, consentendo a più oggetti di gestire la richiesta. Command: incapsula una richiesta come un oggetto. Interpreter: definisce una rappresentazione grammaticale di un linguaggio e fornisce un interprete per elaborarlo. Iterator: fornisce un modo per accedere sequenzialmente agli elementi di una collezione. Mediator: definisce un oggetto che incapsula il modo in cui un insieme di oggetti interagiscono. Memento: consente di salvare e ripristinare lo stato interno di un oggetto senza violare l’incapsulamento. Observer: definisce una dipendenza uno-a-molti tra oggetti in modo che quando un oggetto cambia stato, tutti i suoi dipendenti vengano notificati. State: consente a un oggetto di modificare il suo comportamento quando il suo stato interno cambia. Strategy: definisce una famiglia di algoritmi, li incapsula e li rende intercambiabili. Template Method: definisce lo scheletro di un algoritmo in un metodo, consentendo alle sottoclassi di ridefinire alcuni passaggi. Visitor: rappresenta un’operazione da eseguire su elementi di una struttura di oggetti. 1.4 Design Pattern - I Pattern Creazionali 1.4.1 Il Pattern Factory Method È un design pattern creazionale che fornisce un’interfaccia per creare oggetti in una superclasse, ma consente alle sottoclassi di modificare i tipo di oggetti che verranno creati. È essenzialmente un metodo che viene utilizzato per creare oggetti, ma invece di istanziare direttamente gli oggetti, delega l’istanziazione alle sottoclassi. Questo approccio offre diversi vantaggi: Disaccoppiamento: il codice client che utilizza il Factory Method non ha bisogno di conoscere le classi concrete degli oggetti che sta creando, ma solo l’interfaccia comune che implementano. Questo riduce l’accoppiamento tra il codice client e le classi concrete, rendendo il codice più flessibile e facile da manutenere. Sottoclassi flessibili : le sottoclassi possono decidere quale tipo specifico di oggetto creare, consentendo di estendere il comportamento del codice senza modificare le classi esistenti. Coerenza: Assicura che gli oggetti vengano creati in modo coerente, seguendo le regole definite nella superclasse. Il Factory Method pattern è particolarmente utile quando si ha la necessità di creare oggetti che appartengono a una gerarchia di classi, ma non si vuole che il codice client sia a conoscenza delle classi concrete all’interno della gerarchia. 13 1.4.2 Il Pattern Abstract Factory È un design pattern creazionale che consente di produrre famiglie di oggetti correlati senza specifi- care le loro classi concrete. In sostanza, fornisce un’interfaccia per creare oggetti che sono correlati tra loro, ma senza specificare le classi concrete di questi oggetti. Il problema che questo pattern cerca di risolvere è la necessità di creare oggetti che appartengono insieme (ad esempio, mobili di un determinato stile), ma senza creare dipendenze nel codice tra il ”creatore” di questi oggetti e le classi concrete degli oggetti stessi. Ma come funziona il pattern Abstract Factory? 1. Interfacce astratte: Si definiscono delle interfacce per ogni prodotto che fa parte di una famiglia. Ad esempio, se si sta creando un’applicazione per mobili, si potrebbero avere inter- facce come Sedia, Divano, Tavolino, etc. 2. Factory concreta: Si crea una classe ”factory concreta” per ogni variante della famiglia di prodotti. Ad esempio, si potrebbe avere una FabbricaMobiliModerni che crea oggetti Sedi- aModerna, DivanoModerno, etc. Ogni factory concreta implementa l’interfaccia AbstractFac- tory. 3. utilizzo da parte del client: Il codice client utilizza l’interfaccia AbstractFactory per creare gli oggetti. In questo modo, il client non ha bisogno di sapere quale factory concreta viene utilizzata, né le classi concrete degli oggetti che vengono creati. I vantaggi che il pattern Abstract Factory porta sono diversi: Indipendenza dalle classi concrete: Il codice client è indipendente dalle classi concrete dei prodotti, il che lo rende più facile da mantenere e aggiornare. Coerenza delle famiglie di oggetti : Assicura che gli oggetti creati appartengano alla stessa famiglia (ad esempio, tutti i mobili sono in stile moderno). Semplicità di aggiunta di nuove varianti : È facile aggiungere nuove varianti di famiglie di prodotti semplicemente creando una nuova factory concreta. Mentre il Factory Method si concentra sulla creazione di un singolo tipo di oggetto, Abstract Factory si occupa della creazione di famiglie di oggetti correlati. In altre parole, il Factory Method è un metodo che crea oggetti, mentre Abstract Factory è un oggetto che crea altri oggetti (factory). 1.4.3 Il Pattern Builder È un design pattern creazionale che semplifica la creazione di oggetti complessi passo dopo passo, consentendo la creazione di diverse rappresentazioni di un oggetto con lo stesso codice di costruzione. Il problema principale che il Builder cerca di risolvere è la difficoltà di gestire costruttori con molti parametri, soprattutto quando si ha a che fare con oggetti complessi. Aggiungere continuamente nuovi parametri al costruttore lo rende ingombrante e difficile da usare. Creare sottoclassi per ogni combinazione di parametri può portare ad un’esplosione di classi e a problemi di manutenibilità. Prendiamo l’esempio di una classe House per illustrare questo problema. Una casa può avere molte caratteristiche opzionali, come un giardino, una piscina, un garage, ecc. Gestire tutte queste opzioni con un costruttore tradizionale porterebbe ad un codice inefficiente e difficile da leggere. Il pattern Builder risolve questo problema spostando la logica di creazione in un oggetto separato chiamato 14 builder. Il builder fornisce metodi specifici per impostare ogni parte dell’oggetto complesso, con- sentendo al client di costruire l’oggetto passo dopo passo. Una volta completata la costruzione, il client può ottenere l’oggetto finale dal builder. I vantaggi del pattern Builder sono diversi: Maggiore leggibilità: il codice di costruzione è più chiaro e facile da capire. Flessibilità: permette di creare oggetti con diverse configurazioni in modo semplice. Immutabilità: Può essere utilizzato per creare oggetti immutabili, impostando i valori dei campi solo durante la fase di costruzione. Gli elementi chiave dei Builder sono: Builder: Definisce l’interfaccia per la creazione delle parti dell’oggetto complesso. Concrete Builder: Implementa l’interfaccia Builder e fornisce la logica per la creazione delle parti specifiche. Director: (opzionale) Definisce l’ordine in cui i metodi del Builder devono essere chiamati per creare una specifica configurazione dell’oggetto. Product: Rappresenta l’oggetto complesso che viene costruito. Il Director è una classe opzionale che può essere utilizzata con il Builder per definire sequenze predefinite di passi di costruzione. Ad esempio, potremmo avere un Director chiamato Pizza- MargheritaDirector che chiama i metodi del builder in un ordine specifico per creare una pizza Margherita. 1.4.4 Il Pattern Prototype È un design pattern creazionale che consente di copiare oggetti esistenti senza rendere il codice dipendente dalle loro classi. Invece di creare nuovi oggetti da zero, Prototype clona un oggetto prototipo per ottenere una copia identica. Questo è particolarmente utile quando la creazione di un oggetto è costosa o complessa. Normalmente, per creare una copia di un oggetto, si crea un nuovo oggetto della stessa classe e si copiano i valori di tutti i campi dall’oggetto originale a quello nuovo. Tuttavia, questo approccio presenta due problemi principali: Non è possibile accedere ai campi privati per copiare i loro valori. Il codice diventa dipendente dalla classe concreta dell’oggetto. Il pattern Prototype risolve questi problemi delegando l’operazione di copia all’oggetto stesso. L’oggetto prototipo implementa un metodo clone() che crea una copia di se stesso, inclusi i valori dei campi privati. Il codice client non ha quindi bisogno di conoscere la classe concreta dell’oggetto per clonarlo. Un oggetto che supporta la clonazione è chiamato ”prototipo”. Per utilizzare Proto- type, si crea un insieme di oggetti prototipo preconfigurati con diversi stati. Quando si ha bisogno di un nuovo oggetto, si clona semplicemente il prototipo desiderato. Il pattern Prototype presenta diversi vantaggi: Indipendenza dalla classe concreta: il codice client non ha bisogno di conoscere la classe concreta dell’oggetto per clonarlo. 15 Creazione semplificata di oggetti complessi : la clonazione semplifica la creazione di nuovi oggetti, specialmente se sono complessi o hanno molti campi. Controllo sulla creazione di sottoclassi : consente di definire come le sottoclassi devono creare nuove istanze, garantendo un comportamento coerente. 1.4.5 Il Pattern Singleton È design pattern creazionale che garantisce che una classe abbia una sola istanza e fornisce un punto di accesso globale a tale istanza. Il pattern Singleton affronta due problematiche principali, violando in parte il principio di singola responsabilità per raggiungere l’obiettivo: Garantire l’unicità dell’istanza: In alcuni scenari, è fondamentale che una classe abbia una sola istanza per evitare problemi di coerenza dei dati o per gestire risorse condivise. Ad esempio, se si sta accedendo a un database o a un file di configurazione, è importante avere una sola istanza della classe che gestisce l’accesso a tali risorse per evitare conflitti. Fornire un punto di accesso globale: Il Singleton permette di accedere all’unica istanza della classe da qualsiasi punto del codice, senza dover passare l’oggetto come parametro in contin- uazione. Questo semplifica il codice e lo rende più leggibile. Tuttavia, è importante notare che l’abuso di variabili globali può portare a un codice meno manutenibile. L’implementazione del Singleton si basa su due passaggi fondamentali: 1. Costruttore privato: Il costruttore della classe viene reso privato, impedendo la creazione di istanze della classe dall’esterno tramite l’operatore new. 2. Metodo di creazione statica: Viene definito un metodo statico pubblico, spesso chiam- ato getInstance(), che funge da ”costruttore” alternativo. Questo metodo verifica se esiste già un’istanza della classe. Se esiste, la restituisce. Altrimenti, crea una nuova istanza, la memorizza in un campo statico privato e la restituisce. I vantaggi del pattern Singleton sono diversi: Controllo sull’istanziazione: Garantisce che venga creata una sola istanza della classe. Accesso globale semplificato: Fornisce un punto di accesso globale all’unica istanza. Miglioramento della leggibilità del codice: Elimina la necessità di passare l’oggetto come parametro in continuazione. Quando si utilizza il pattern Singleton bisogna però fare attenzione ad alcuni aspetti: Sovrautilizzo: Il Singleton non dovrebbe essere utilizzato eccessivamente. Se non è stretta- mente necessario avere una sola istanza di una classe, è meglio evitare di utilizzare questo pattern. Testing: Testare il codice che utilizza il Singleton può essere più complesso, poiché si ha a che fare con un oggetto globale. Alternative: Esistono alternative al pattern Singleton, come l’iniezione di dipendenze, che possono essere più adatte in alcuni casi. 16 1.5 Design Pattern - I Pattern Strutturali 1.5.1 Il Pattern Adapter È un design pattern strutturale che permette a due oggetti con interfacce incompatibili di lavo- rare insieme. Immagina di avere un oggetto che fornisce dati in un formato specifico e un al- tro oggetto che invece si aspetta i dati in un formato diverso. In questo caso, l’Adapter funge da intermediario, convertendo i dati da un formato all’altro e consentendo ai due oggetti di co- municare. L’Adapter è una soluzione a problemi di compatibilità tra classi che altrimenti non potrebbero collaborare. Questo pattern è particolarmente utile quando si desidera integrare una classe esistente (l’adattato) con un’altra classe (il client) che ha un’interfaccia diversa. Il pattern Adapter si basa su un oggetto adattatore che implementa l’interfaccia attesa dal client e, allo stesso tempo, mantiene un riferimento all’oggetto adattato. Quando il client chiama un metodo dell’adattatore, quest’ultimo traduce la richiesta in un formato comprensibile all’oggetto adattato e delega l’esecuzione del metodo. L’adattatore può anche convertire i dati di ritorno dall’oggetto adattato in un formato compatibile con il client. Possiamo prendere come esempio un’applicazione per il monitoraggio del mercato azionario che utilizza una libreria di analisi che accetta dati solo in formato JSON. Tuttavia, l’applicazione riceve i dati azionari in formato XML. Per risolvere questa incompatibilità, viene creato un adattatore XML-to-JSON che converte i dati XML in JSON prima di passarli alla libreria di analisi. Esistono due tipi principali di Adapter: Adapter di oggetti: Questo tipo di Adapter utilizza la composizione di oggetti. L’adattatore mantiene un riferimento all’oggetto adattato e implementa l’interfaccia del client delegando le chiamate all’oggetto adattato. Adapter di classe: Questo tipo di Adapter utilizza l’ereditarietà multipla. L’adattatore eredita sia dall’interfaccia del client che dalla classe dell’oggetto adattato. I vantaggi del pattern Adapter sono diversi: Riuso del codice: Permette di riutilizzare classi esistenti che altrimenti non sarebbero com- patibili. Flessibilità: Facilita l’introduzione di nuove classi o librerie senza dover modificare il codice esistente. Trasparenza: Il client non è a conoscenza dell’oggetto adattato, semplificando l’utilizzo del codice. 1.5.2 Il Pattern Bridge È un design pattern strutturale che, come suggerisce il nome, crea un ponte tra due gerarchie di classi separate: astrazione e implementazione. Invece di utilizzare l’ereditarietà per combinare funzionalità, Bridge utilizza la composizione di oggetti, delegando parte del lavoro a un oggetto separato che fa parte della gerarchia di implementazione. Questo permette di modificare o estendere le due gerarchie in modo indipendente, senza influire l’una sull’altra. Il pattern Bridge risolve il problema dell’esplosione di sottoclassi, che si verifica quando si cerca di estendere una classe lungo più dimensioni. Ad esempio, se si ha una classe Forma con sottoclassi Cerchio e Quadrato, e si desidera aggiungere il colore come altra dimensione, si finirebbe con una proliferazione di sottoclassi 17 come CerchioRosso, CerchioBlu, QuadratoRosso, QuadratoBlu, e cosı̀ via. Questo rende il codice difficile da gestire e mantenere. Bridge propone di estrarre una delle dimensioni in una gerarchia separata. Nell’esempio delle forme, la gerarchia del colore potrebbe essere estratta in una nuova classe Colore con sottoclassi Rosso, Blu, ecc. La classe Forma avrebbe quindi un campo che punta a un oggetto Colore. In questo modo, l’aggiunta di nuovi colori non richiederebbe la modifica della gerarchia delle forme e viceversa. Vengono usati i termini ”astrazione” e ”implementazione” per descrivere le due gerarchie di classi. L’astrazione rappresenta il livello di controllo di alto livello, mentre l’implementazione si occupa dei dettagli concreti. Nell’esempio delle forme, la classe Forma sarebbe l’astrazione e la classe Colore l’implementazione. Un esempio concreto di Bridge è la creazione di interfacce utente multipiattaforma. Questo pattern permetta di separare la logica dell’interfaccia utente (l’astrazione) dalle API specifiche del sistema operativo (l’implementazione). In questo modo, la stessa interfaccia utente può es- sere utilizzata su Windows, Linux o altri sistemi operativi semplicemente cambiando l’oggetto di implementazione a cui fa riferimento. I vantaggi che il pattern Bridge offre sono diversi: Disaccoppiamento: Separa l’astrazione dall’implementazione, consentendo di modificarle in- dipendentemente. Estensibilità: Facilita l’aggiunta di nuove funzionalità senza modificare il codice esistente. Semplificazione del codice: Rende il codice più facile da capire e gestire, evitando l’esplosione di sottoclassi. 1.5.3 Il Pattern Composite È un design pattern strutturale che consente di comporre oggetti in strutture ad albero e poi lavorare con queste strutture come se fossero singoli oggetti. Questo pattern è utile quando il modello di base dell’applicazione può essere rappresentato come un albero, ad esempio una struttura di prodotti e scatole, dove una scatola può contenere sia prodotti che altre scatole, creando una gerarchia. Il vantaggio principale del pattern Composite è che permette di trattare oggetti singoli e gruppi di oggetti allo stesso modo. Questo significa che il codice client non ha bisogno di sapere se sta interagendo con un oggetto semplice o con un oggetto composito, semplificando l’interazione con strutture complesse. Nella pratica il pattern Composite lavora in questo modo: Interfaccia comune: Si definisce un’interfaccia comune per tutti gli oggetti che compongono l’albero, sia che si tratti di oggetti semplici (foglie) o di oggetti compositi (nodi). Questa interfaccia dovrebbe includere i metodi comuni a tutti gli oggetti, come ad esempio un metodo per calcolare il prezzo totale. Oggetti foglia: Gli oggetti foglia implementano l’interfaccia comune e forniscono l’implementazione concreta dei metodi. Ad esempio, un oggetto Prodotto avrebbe un metodo calcolaPrezzoTotale che restituisce semplicemente il prezzo del prodotto. Oggetti compositi: Gli oggetti compositi, come ad esempio un oggetto Scatola, implemen- tano anch’essi l’interfaccia comune. Tuttavia, invece di fornire un’implementazione diretta dei metodi, iterano sui loro figli (prodotti o altre scatole) e chiamano lo stesso metodo su di essi, aggregando i risultati. Ad esempio, il metodo calcolaPrezzoTotale di Scatola chiamerebbe 18 calcolaPrezzoTotale su ogni prodotto o scatola contenuta, sommando i prezzi e aggiungendo eventuali costi aggiuntivi. Utilizzando il pattern Composite, il codice client può trattare un intero albero di oggetti come un singolo oggetto. Ad esempio, per calcolare il prezzo totale di un ordine, il client può semplicemente chiamare il metodo calcolaPrezzoTotale sull’oggetto radice dell’albero, senza doversi preoccupare della sua struttura interna. Il pattern Composite è utilizzabile ad esempio nell’implementazione di un file system, che può essere rappresentato come un albero di oggetti, con le directory radici e le sottodirectory come nodi e foglie 1.5.4 Il pattern Decorator È un design pattern strutturale che consente di aggiungere dinamicamente nuove responsabilità a un oggetto, posizionandolo all’interno di speciali oggetti wrapper che contengono i comportamenti aggiuntivi. Questo pattern offre un’alternativa flessibile all’ereditarietà per estendere le funzionalità di un oggetto a runtime. Il pattern Decorator funziona in questo modo: Componente: Definisce l’interfaccia comune per tutti gli oggetti, sia concreti che decorati. Oggetto concreto: Rappresenta l’oggetto a cui si desidera aggiungere nuove responsabilità. Decoratore di base: Implementa l’interfaccia del componente e contiene un riferimento all’oggetto concreto da decorare. Il decoratore di base delega le richieste all’oggetto concreto, ma può anche aggiungere il proprio comportamento prima o dopo la delega. Decoratori concreti: Estendono il decoratore di base e aggiungono nuove responsabilità specifiche all’oggetto concreto. Il pattern Decorator consente di aggiungere e rimuovere responsabilità in modo dinamico, senza modificare il codice dell’oggetto concreto o creare un’esplosione di sottoclassi. Questo lo rende ideale per situazioni in cui le funzionalità di un oggetto devono essere estese in modo flessibile e configurabile. Ad esempio, immaginiamo un’applicazione per la creazione di notifiche. Invece di creare sottoclassi per ogni combinazione di canali di notifica (email, SMS, notifiche push), possiamo utilizzare il pattern Decorator per aggiungere dinamicamente i canali di notifica desiderati a un oggetto Notificatore di base. Ecco alcuni altri esempi di utilizzo del pattern Decorator: Stream di input/output: In Java, le classi FileInputStream, BufferedInputStream e DataIn- putStream sono esempi di decoratori che aggiungono funzionalità a un flusso di input di base. Interfacce utente grafiche: I bordi, le barre di scorrimento e altri elementi dell’interfaccia utente possono essere implementati come decoratori che aggiungono nuove funzionalità ai componenti di base. In sintesi, il pattern Decorator offre i seguenti vantaggi: Flessibilità: Permette di aggiungere e rimuovere responsabilità in modo dinamico. Evitare l’esplosione di sottoclassi : Offre un’alternativa all’ereditarietà per l’estensione delle funzionalità degli oggetti. 19 Responsabilità singola: Separa le responsabilità di base di un oggetto dalle responsabilità aggiuntive. 1.5.5 Il Pattern Facade È un design pattern strutturale che fornisce un’interfaccia semplificata per un sottosistema com- plesso, come una libreria, un framework o un insieme di classi. L’obiettivo principale è quello di nascondere la complessità del sottosistema al client, offrendogli un’interfaccia più facile da usare e comprendere. Il pattern Facade è particolarmente utile quando si ha a che fare con sottosistemi che hanno molte parti mobili e un’interfaccia complessa. Invece di costringere il client a interagire direttamente con tutte le classi e le interfacce del sottosistema, la Facade fornisce un punto di ac- cesso unificato che gestisce la complessità interna e presenta solo le funzionalità essenziali al client. Il pattern Facade funziona in questo modo: La Facade definisce un’interfaccia semplificata per il sottosistema. Questa interfaccia dovrebbe includere solo i metodi e le funzionalità che sono rilevanti per il client. La Facade implementa l’interfaccia e si occupa di interagire con le classi del sottosistema, nascondendo la complessità al client. Il client interagisce con il sottosistema solo attraverso la Facade. L’utilizzo del pattern Facade porta diversi vantaggi: Semplifica l’utilizzo del sottosistema: Il client non ha bisogno di conoscere i dettagli di imple- mentazione del sottosistema. Riduce l’accoppiamento tra il client e il sottosistema: Le modifiche al sottosistema non dovreb- bero influenzare il client, a patto che l’interfaccia della Facade rimanga invariata. Migliora la leggibilità e la manutenibilità del codice: Il codice client che utilizza la Facade è più pulito e facile da capire. Immaginiamo un’applicazione che carica brevi video divertenti con i gatti sui social media. Questa applicazione potrebbe utilizzare una libreria di conversione video professionale per codi- ficare i video in diversi formati. Tuttavia, l’applicazione ha bisogno solo di una funzione molto specifica della libreria: la codifica di un video in un determinato formato. In questo caso, il pat- tern Facade può essere utilizzato per creare una classe VideoConverter che nasconde la complessità della libreria di conversione video. La classe VideoConverter avrebbe un metodo encode(filename, format) che accetta il nome del file video e il formato di output desiderato. Internamente, la classe VideoConverter si occuperebbe di interagire con la libreria di conversione video, ma il client non avrebbe bisogno di conoscere i dettagli di questa interazione. 1.5.6 Il Pattern Flyweight È un design pattern strutturale che consente di utilizzare più oggetti in modo efficiente, anche quando sono presenti molti oggetti simili con dati in gran parte identici. Invece di memorizzare tutti i dati in ogni oggetto, il pattern Flyweight suggerisce di suddividere lo stato dell’oggetto in due parti: 20 Stato intrinseco: contiene dati che sono immutabili e condivisi tra più oggetti. Ad esem- pio, il colore e lo sprite di un proiettile in un videogioco potrebbero essere considerati stato intrinseco, poiché tutti i proiettili di un certo tipo hanno in genere lo stesso aspetto. Stato estrinseco: contiene dati che sono univoci per ogni oggetto e possono cambiare nel tempo. Ad esempio, la posizione, la direzione e la velocità di un proiettile in un videogioco sarebbero considerate stato estrinseco. Il pattern Flyweight propone di memorizzare lo stato intrinseco in un numero limitato di oggetti condivisi, chiamati flyweight, mentre lo stato estrinseco viene memorizzato esternamente e passato ai metodi dei flyweight quando necessario. Nel nostro esempio del videogioco, invece di creare un nuovo oggetto Proiettile per ogni proiettile sullo schermo, potremmo creare un solo oggetto ProiettileFlyweight che memorizza lo stato intrinseco (colore e sprite). Ogni proiettile sullo schermo sarebbe quindi rappresentato da un piccolo oggetto contesto che memorizza lo stato estrinseco (posizione, direzione, velocità) e un riferimento al ProiettileFlyweight. Questo approccio consente di ridurre significativamente il consumo di memoria, poiché lo stato intrinseco viene memorizzato solo una volta. Inoltre, poiché i flyweight sono immutabili, possono essere condivisi in modo sicuro tra più oggetti senza il rischio di incoerenze. I vantaggi del pattern Flyweight: Risparmio di memoria: Riduce il numero di oggetti creati, risparmiando memoria. Miglioramento delle prestazioni : Riduce il tempo necessario per creare e gestire gli oggetti. Riduzione della complessità del codice: Separa lo stato intrinseco da quello estrinseco, sem- plificando la gestione degli oggetti. Gli svantaggi del pattern Flyweight: Aumento della complessità di implementazione del codice: L’implementazione del pattern Flyweight può essere complessa, soprattutto se il numero di oggetti condivisi è elevato. Difficoltà di condivisione dello stato: Lo stato estrinseco deve essere gestito con attenzione per evitare incoerenze. 1.5.7 Il Pattern Proxy È un design pattern strutturale che fornisce un surrogato o un segnaposto per un altro oggetto, chiamato ”oggetto reale” o ”oggetto servizio”. Il proxy controlla l’accesso all’oggetto reale, consen- tendo di eseguire operazioni prima o dopo che la richiesta raggiunge l’oggetto reale. Lo scopo del pattern Proxy è: Controllo dell’accesso: Il proxy può limitare l’accesso all’oggetto reale, ad esempio consen- tendo l’accesso solo a utenti autorizzati. Lazy initialization: Il proxy può creare l’oggetto reale solo quando è effettivamente neces- sario, risparmiando risorse. Caching: Il proxy può memorizzare i risultati delle operazioni sull’oggetto reale, evitando di doverle ripetere. 21 Logging e debugging: Il proxy può registrare le chiamate all’oggetto reale, aiutando a identificare problemi. Aggiunta di funzionalità: Il proxy può aggiungere nuove funzionalità all’oggetto reale senza modificare il suo codice. Il pattern Proxy funziona in questo modo: Oggetto reale (Subject): L’oggetto a cui si desidera controllare l’accesso. Interfaccia (Subject): Un’interfaccia comune per l’oggetto reale e il proxy, che consente al client di utilizzare entrambi in modo trasparente. Proxy: Il sostituto dell’oggetto reale. Il proxy implementa la stessa interfaccia dell’oggetto reale e contiene un riferimento ad esso. Client: L’oggetto che utilizza l’oggetto reale o il proxy. Il client interagisce con l’oggetto reale o il proxy attraverso l’interfaccia comune. Quando il client chiama un metodo sul proxy, il proxy controlla se può gestire la richiesta da solo. Se sı̀, esegue l’operazione richiesta e restituisce il risultato al client. Se no, il proxy inoltra la richiesta all’oggetto reale. Il pattern Proxy ci permette di nascondere la complessità dell’oggetto reale al client, controllare l’accesso agli oggetti reali, e di modificarne o estendere le funzionalità senza però andare effettiva- mente a modificare il codice dell’oggetto reale o del client. 1.6 Design Pattern - I Pattern Comportamentali 1.6.1 Il Pattern Chain of Responsability È un design pattern comportamentale che permette di passare le richieste lungo una catena di gestori. Al ricevimento di una richiesta, ogni gestore decide di elaborarla o di passarla al succes- sivo gestore della catena. Questo pattern evita di accoppiare il mittente di una richiesta al suo destinatario, dando la possibilità a più di un oggetto di gestirla. Invece di elaborare la richiesta in un unico punto, essa viene passata lungo una catena di oggetti ”handler ” fino a quando uno di essi non è in grado di gestirla. Immaginiamo un’applicazione che debba applicare diversi controlli sequenziali ad una richiesta, come l’autenticazione utente, la sanitizzazione dei dati e la gestione della cache. Invece di inserire tutta la logica di controllo in un unico blocco di codice, il pattern Chain of Responsibility suggerisce di creare una serie di oggetti ”handler”, ognuno responsabile di un singolo controllo. Ogni handler, dopo aver esaminato la richiesta, può decidere se: Gestire la richiesta: in questo caso, l’handler elabora la richiesta e la catena si interrompe. Passare la richiesta al successivo handler nella catena: se l’handler corrente non è in grado di gestire la richiesta, la passa al successivo handler nella catena. L’utilizzo del pattern Chain of Responsability offre diversi vantaggi: Modularità e riutilizzo: ogni handler rappresenta un’unica unità di funzionalità, rendendo il codice più facile da capire, modificare e riutilizzare in diverse parti dell’applicazione. 22 Flessibilità: è possibile aggiungere o rimuovere handler dalla catena dinamicamente in fase di esecuzione senza modificare il codice esistente. Disaccoppiamento: il mittente della richiesta non deve essere a conoscenza di quale handler gestirà effettivamente la richiesta. Un esempio di Chain of Responsibility è la gestione degli eventi in un’interfaccia utente. Quando un utente interagisce con un elemento GUI, l’evento viene propagato lungo una catena di oggetti (ad esempio, dal pulsante al suo contenitore e infine alla finestra principale) fino a quando non viene gestito. Il pattern Chain of Responsibility è utile in situazioni in cui è necessario eseguire una sequenza di operazioni su una richiesta, ma l’ordine o il numero di operazioni può variare, oppure quando si desidera disaccoppiare il mittente di una richiesta dal suo destinatario, consentendo a più oggetti di avere la possibilità di gestirla. Tuttavia, il pattern Chain of Responsibility può diventare inefficiente se la catena di handler è troppo lunga o se la richiesta deve essere elaborata da tutti gli handler nella catena. 1.6.2 Il Pattern Command È un design pattern comportamentale che trasforma una richiesta in un oggetto a sé stante, consen- tendo di parametrizzare i client con diverse richieste, accodare o registrare le richieste e supportare operazioni annullabili. Il problema che il pattern Command si propone di risolvere è quello di avere troppe sottoclassi di un oggetto GUI (come un pulsante) per implementare diversi comportamenti di click. Questo approccio porta ad una forte dipendenza tra il codice GUI e il codice della logica di business, rendendo il codice difficile da mantenere e riutilizzare. Il pattern Command risolve questo problema estraendo i dettagli di una richiesta in una classe Command separata. Questo oggetto Command ha un unico metodo, execute(), che incapsula la richiesta. Gli oggetti GUI (Invoker) possono quindi essere parametrizzati con diversi oggetti Command, rendendoli indipendenti dalla logica di business specifica. L’ulitto del pattern Command porta diversi vantaggi: Disaccoppiamento: gli oggetti GUI (Invoker) non devono conoscere la logica di business speci- fica (Receiver) o come viene eseguita la richiesta. Flessibilità: è facile aggiungere nuovi comandi senza modificare il codice esistente. Supporto per l’annullamento/ripristino: memorizzando i comandi eseguiti, è possibile an- nullare o ripristinare le operazioni eseguite in precedenza. Un esempio di utilizzo del pattern Command può essere l’ordinazione di un pasto in un ristorante. Il cliente (Invoker) effettua un ordine (Command) al cameriere (Receiver). L’ordine contiene tutti i dettagli necessari per preparare il pasto, e il cameriere non ha bisogno di conoscere i dettagli specifici della preparazione. L’ordine può essere messo in coda fino a quando lo chef non è pronto a prepararlo. 1.6.3 Il Pattern Iterator È un design pattern comportamentale che consente di scorrere gli elementi di una collezione senza esporne la rappresentazione sottostante (lista, stack, albero, ecc.). Questo pattern è utile quando si ha la necessità di accedere agli elementi di una collezione in modo sequenziale, ma non si vuole conoscere i dettagli della sua implementazione. Il problema principale che il pattern Iterator cerca 23 di risolvere è la difficoltà di scrivere codice generico per l’attraversamento di collezioni con strut- ture dati differenti. Ad esempio, l’attraversamento di un array è diverso dall’attraversamento di un albero, e l’aggiunta di nuovi algoritmi di attraversamento ad una collezione può renderla troppo complessa. Il pattern Iterator risolve questo problema introducendo un oggetto separato, chiamato iteratore, che incapsula la logica di attraversamento della collezione. L’iteratore for- nisce un’interfaccia semplice per accedere agli elementi della collezione in sequenza, senza esporre i dettagli della sua implementazione. Questo permette al codice client di essere indipendente dalla struttura dati sottostante della collezione e di utilizzare lo stesso codice per attraversare collezioni differenti. Un’analogia con il mondo reale potrebbe essere: una guida turistica. La guida turistica può essere vista come un iteratore per una collezione di luoghi di interesse. Il turista (codice client) non ha bisogno di conoscere l’ordine dei luoghi o come raggiungerli. La guida si occupa di questi dettagli, fornendo al turista un modo semplice per visitare ogni luogo in sequenza. 1.6.4 Il Pattern Mediator È un design pattern comportamentale che fornisce un modo per centralizzare la comunicazione e la collaborazione tra oggetti che altrimenti sarebbero strettamente accoppiati. Invece di interagire direttamente tra loro, gli oggetti comunicano attraverso un oggetto Mediator, che funge da in- termediario. Quando più oggetti in un sistema comunicano direttamente tra loro, si crea un forte accoppiamento tra di essi. Ciò significa che le modifiche apportate a un oggetto possono avere un impatto a cascata su molti altri oggetti nel sistema, rendendo il codice difficile da mantenere ed evolvere. Ad esempio, in un’interfaccia utente grafica, diversi componenti come pulsanti, campi di testo e menu a tendina potrebbero dover interagire tra loro. Se ogni componente comunica di- rettamente con gli altri, il codice diventa rapidamente complesso e difficile da gestire. Il pattern Mediator risolve questo problema introducendo un oggetto Mediator, che incapsula tutta la logica di comunicazione e collaborazione tra gli oggetti. Gli oggetti, noti come Colleague, non comuni- cano più direttamente tra loro, ma solo con il Mediator. Quando un Colleague deve interagire con un altro, invia una richiesta al Mediator. Il Mediator elabora la richiesta e la inoltra al Colleague appropriato. In questo modo, il Mediator agisce come un intermediario, disaccoppiando i Colleague e semplificando la comunicazione. L’utilizzo del pattern Mediator porta diversi vantaggi: Disaccoppiamento: gli oggetti Colleague non sono più direttamente accoppiati tra loro, il che rende più facile modificare, estendere o riutilizzare singoli componenti senza influire sul resto del sistema. Semplificazione della comunicazione: il Mediator centralizza la logica di comunicazione, ren- dendo il codice più facile da comprendere e mantenere. Maggiore flessibilità: le regole di comunicazione e collaborazione possono essere modificate centralmente nel Mediator, senza dover modificare i Colleague. Ecco alcuni esempi di utilizzo del pattern Mediator: Finestra di dialogo: In un’interfaccia utente grafica, una finestra di dialogo può fungere da Mediator per i suoi vari componenti, come pulsanti, campi di testo e caselle di controllo. Quando un utente interagisce con un componente, la finestra di dialogo riceve la notifica 24 e intraprende le azioni appropriate, come la convalida dei dati immessi o la chiusura della finestra di dialogo stessa. Controllo del traffico aereo: I piloti degli aerei in avvicinamento o in partenza da un aeroporto non comunicano direttamente tra loro, ma attraverso una torre di controllo del traffico aereo, che funge da Mediator. La torre di controllo riceve informazioni da tutti gli aeromobili e fornisce istruzioni per garantire un flusso di traffico sicuro ed efficiente. 1.6.5 Il Pattern Memento È un design pattern comportamentale che consente di catturare e salvare lo stato interno di un oggetto senza violare l’incapsulamento. Ciò consente di ripristinare l’oggetto a uno stato precedente in un momento successivo. Quando si lavora con oggetti che hanno uno stato interno complesso, può essere necessario fornire un modo per annullare le modifiche apportate all’oggetto o per ripristinare l’oggetto a uno stato precedente. Tuttavia, l’accesso diretto allo stato interno di un oggetto può violare l’incapsulamento, un principio fondamentale della programmazione orientata agli oggetti. L’incapsulamento implica nascondere i dati interni di un oggetto e fornire metodi pubblici per interagire con tali dati. Consentire ad altri oggetti di accedere direttamente allo stato interno di un oggetto può portare a dipendenze indesiderate e rendere il codice più fragile e difficile da mantenere. Il pattern Memento risolve questo problema introducendo tre oggetti: Originator: l’oggetto che ha uno stato interno che deve essere salvato e ripristinato. Memento: un oggetto che memorizza lo stato interno dell’Originator. Il Memento è un oggetto opaco per gli altri oggetti, il che significa che solo l’Originator può accedere al suo stato interno. Caretaker: un oggetto responsabile della memorizzazione e del recupero dei Memento. Il Caretaker non ha accesso allo stato interno del Memento e lo tratta come una scatola nera. Quando l’Originator deve salvare il suo stato, crea un nuovo oggetto Memento e copia il suo stato interno nel Memento. Il Memento viene quindi passato al Caretaker per la memorizzazione. Quando l’Originator deve ripristinare il suo stato precedente, richiede il Memento appropriato al Caretaker. Il Caretaker restituisce il Memento all’Originator e l’Originator copia lo stato interno dal Memento al suo stato interno corrente. Ecco alcuni esempi concreti dell’utilizzo del pattern Memento: Editor di testo: L’editor di testo è l’Originator. Ogni volta che l’utente esegue un’operazione che modifica lo stato del documento (ad esempio, inserimento o cancellazione di testo), l’editor crea un nuovo Memento che cattura lo stato corrente del documento, inclusi il contenuto del testo, la posizione del cursore e altre informazioni rilevanti. Cronologia degli stati : Un oggetto ”Cronologia” funge da Caretaker. La cronologia memorizza una pila di Memento, ogni Memento rappresenta uno stato precedente del documento. Quando l’utente esegue l’operazione di annullamento, la cronologia fornisce all’editor il Memento più recente, consentendo di ripristinare lo stato precedente del documento. È importante notare che il pattern Memento può comportare un sovraccarico in termini di memoria, soprattutto se gli oggetti Originator sono di grandi dimensioni o se vengono creati molti 25 Memento. Pertanto, è necessario valutare attentamente l’utilizzo del pattern e considerare strategie per ottimizzare la gestione dei Memento, come la memorizzazione solo delle differenze tra gli stati o l’utilizzo di tecniche di memorizzazione efficienti. 1.6.6 Il Pattern Observer È un design pattern comportamentale che stabilisce una relazione di tipo ”uno-a-molti” tra oggetti. In questo pattern, un oggetto, chiamato soggetto, mantiene una lista di oggetti dipendenti, chia- mati osservatori, e li notifica automaticamente di qualsiasi cambiamento del suo stato, in genere chiamando uno dei loro metodi. Il problema principale che il pattern Observer cerca di risolvere è la necessità di mantenere la coerenza tra oggetti correlati senza creare un forte accoppiamento tra di essi. Ad esempio, in un’applicazione con un’interfaccia utente grafica, è necessario aggiornare la visualizzazione ogni volta che i dati sottostanti cambiano. Tuttavia, non si vuole che gli oggetti dati siano strettamente accoppiati agli oggetti dell’interfaccia utente, perché ciò renderebbe il codice difficile da mantenere e riutilizzare. Un esempio concreto di questo problema riguarda la relazione tra un cliente e un negozio. Il cliente è interessato all’arrivo di un nuovo prodotto e potrebbe dover controllare frequentemente la disponibilità del prodotto presso il negozio. Tuttavia, questo approc- cio richiede al cliente di essere a conoscenza del sistema di gestione dell’inventario del negozio e di controllarlo costantemente, creando un’inefficienza e un accoppiamento indesiderato. L’utilizzo del pattern Observer porta diversi vantaggi: Disaccoppiamento: Il pattern Observer riduce l’accoppiamento tra il soggetto e i suoi osser- vatori, consentendo di modificarli o estenderli indipendentemente l’uno dall’altro. Flessibilità: È possibile aggiungere o rimuovere osservatori in qualsiasi momento, anche du- rante l’esecuzione, senza dover modificare il codice del soggetto o degli altri osservatori. Scalabilità: Il pattern Observer può essere utilizzato per gestire un numero elevato di osser- vatori senza compromettere le prestazioni del sistema. L’implementazione specifica del pattern Observer può variare a seconda del linguaggio di pro- grammazione e delle esigenze specifiche dell’applicazione. Tuttavia, l’idea generale di stabilire una relazione di tipo ”uno-a-molti” tra oggetti per propagare le notifiche di cambiamento di stato rimane la stessa. Ad esempio, alcuni framework e librerie potrebbero fornire implementazioni predefinite del pattern Observer, semplificando ulteriormente l’adozione di questo pattern nelle applicazioni. 1.6.7 Il Pattern State È un design pattern comportamentale che permette a un oggetto di modificare il suo comportamento a runtime a seconda del suo stato interno. Invece di implementare la logica per tutti i possibili stati all’interno della classe principale dell’oggetto, il pattern State delega il comportamento a una serie di classi di stato separate, una per ogni stato possibile. Spesso, gli oggetti hanno uno stato interno che influenza il modo in cui rispondono ai messaggi o eseguono determinate operazioni. Implementare tutta la logica per gestire questi diversi stati direttamente nella classe dell’oggetto può portare a classi molto grandi e complesse, con molte istruzioni condizionali (come if o switch) per gestire i diversi casi. Questo tipo di codice diventa rapidamente difficile da leggere, da capire e da mantenere, soprattutto quando si aggiungono nuovi stati o comportamenti. Un esempio concreto di questo problema, è quello di un documento che può 26 trovarsi in diversi stati, come ”Bozza”, ”In moderazione” e ”Pubblicato”. Il comportamento del metodo publish del documento varia a seconda dello stato corrente: In stato ”Bozza”, il documento viene spostato in ”In moderazione”. In stato ”In moderazione”, il documento viene pubblicato solo se l’utente corrente è un am- ministratore. In stato ”Pubblicato”, il metodo non fa nulla. Implementare questa logica tramite istruzioni condizionali all’interno del metodo publish può fun- zionare per un numero limitato di stati, ma il codice diventa rapidamente ingombrante e difficile da gestire man mano che si aggiungono nuovi stati o comportamenti. Il pattern State risolve questo problema incapsulando ogni stato in una classe separata. Ciascuna classe di stato implementa un’interfaccia comune che definisce i metodi per tutti i comportamenti possibili dell’oggetto, come publish nell’esempio del documento. L’oggetto principale, chiamato contesto, mantiene un riferimento a un oggetto di stato corrente e delega le richieste a quell’oggetto. Quando lo stato interno del contesto cambia, esso cambia semplicemente l’oggetto di stato corrente con un’istanza di una classe diversa che rappresenta il nuovo stato. In questo modo, l’oggetto cambia effettivamente il suo comportamento a runtime senza dover modificare la sua classe. Ad esempio, nel caso del documento, si avrebbero le classi DraftState, ModerationState e PublishedState. Ciascuna di queste classi implementerebbe l’interfaccia DocumentState con il metodo publish. La classe Document (il contesto) avrebbe un campo state di tipo DocumentState che punta all’oggetto di stato corrente. Quando viene chiamato il metodo publish su Document, esso a sua volta chiama il metodo publish sull’oggetto di stato corrente, delegando la logica specifica dello stato alla classe appropriata. L’utilizzo del pattern State porta diversi vantaggi: Maggiore chiarezza e leggibilità del codice: Separa la logica specifica dello stato in classi distinte, rendendo il codice più facile da comprendere e mantenere. Facilità di aggiungere nuovi stati : Per aggiungere un nuovo stato, è sufficiente creare una nuova classe di stato che implementa l’interfaccia comune. Non è necessario modificare il contesto o le altre classi di stato. Migliore gestione delle transizioni di stato: Le transizioni di stato possono essere gestite in modo esplicito all’interno delle classi di stato, rendendo il flusso di controllo più chiaro. Il pattern State è potente, ma è importante utilizzarlo con attenzione. L’introduzione di troppe classi di stato può rendere il codice eccessivamente complesso e difficile da gestire. È importante valutare se i vantaggi del pattern State superano la complessità aggiuntiva introdotta dall’aggiunta di nuove classi al sistema. 1.6.8 Il Pattern Strategy È un design pattern comportamentale che permette di definire una famiglia di algoritmi, incapsularli in classi separate e renderli intercambiabili. In questo modo, il client può scegliere l’algoritmo da utilizzare a runtime, senza essere legato a una specifica implementazione. Quando si devono implementare diverse varianti di un algoritmo o di un comportamento, si può essere tentati di utilizzare istruzioni condizionali (come if o switch) per selezionare l’implementazione 27 appropriata. Tuttavia, questo approccio può portare a classi grandi e complesse, con molto codice duplicato e difficile da manutenere. Inoltre, l’aggiunta di nuovi algoritmi o comportamenti può richiedere di modificare il codice esistente, aumentando il rischio di introdurre errori. Un esem- pio concreto di questo problema nel contesto di un’applicazione per la navigazione. Inizialmente, l’app supportava solo la creazione di percorsi per auto. Tuttavia, con il tempo sono state aggiunte nuove funzionalità, come la possibilità di creare percorsi a piedi, con i mezzi pubblici, in bicicletta e persino attraverso attrazioni turistiche. Ogni nuova funzionalità ha comportato l’aggiunta di nuova logica al codice di routing, rendendolo sempre più complesso e difficile da gestire. Il pattern Strategy risolve questo problema incapsulando ogni algoritmo o comportamento in una classe separata, chiamata strategia. Tutte le strategie implementano la stessa interfaccia, che definisce il metodo o i metodi per eseguire l’algoritmo o il comportamento desiderato. In questo modo, il client può utilizzare qualsiasi strategia senza dover conoscere i dettagli della sua implemen- tazione. Nel caso dell’applicazione di navigazione, ogni algoritmo di routing (per auto, a piedi, ecc.) sarebbe implementato come una classe separata, che implementa l’interfaccia RoutingStrategy con il metodo buildRoute. La classe principale dell’applicazione, che gestisce la logica di navigazione, avrebbe un campo routingStrategy di tipo RoutingStrategy, che punta all’oggetto strategia corrente. Il client (ad esempio, un pulsante nell’interfaccia utente) può quindi impostare la strategia desiderata nel contesto (la classe principale dell’applicazione). Quando il client richiede un percorso, il contesto delega la richiesta all’oggetto strategia corrente, che si occupa di costruire il percorso utilizzando l’algoritmo appropriato. I vantaggi dell’utilizzo del pattern Strategy sono diversi: Maggiore flessibilità e riusabilità: Il client può scegliere l’algoritmo da utilizzare a runtime, consentendo di adattare il comportamento dell’applicazione alle esigenze specifiche. Inoltre, le strategie possono essere facilmente riutilizzate in altri contesti. Migliore manutenibilità: Le modifiche a un algoritmo o a un comportamento non influenzano le altre parti del codice, riducendo il rischio di introdurre errori. Migliore leggibilità: Il codice diventa più facile da leggere e da capire, poiché la logica per ogni algoritmo è incapsulata in una classe separata. Il pattern Strategy è molto versatile e può essere utilizzato in una varietà di situazioni in cui è necessario implementare diverse varianti di un algoritmo o di un comportamento. Tuttavia, è importante utilizzarlo con attenzione, evitando di creare un numero eccessivo di strategie che potrebbero rendere il codice difficile da gestire. È importante sottolineare che il pattern Strategy può essere simile al pattern State, ma c’è una differenza fondamentale tra i due. Nel pattern State, gli stati possono essere consapevoli l’uno dell’altro e avviare transizioni tra loro. Mentre le strategie nel pattern Strategy in genere non sono consapevoli l’una dell’altra. 1.6.9 Il Pattern Template Method È un design pattern comportamentale che definisce lo scheletro di un algoritmo in una superclasse, lasciando che le sottoclassi ne ridefiniscano alcuni passi senza modificarne la struttura. In altre parole, definisce un’operazione in termini di una serie di passi, alcuni dei quali possono essere astratti e lasciati alle sottoclassi per implementarli. Ciò consente di riutilizzare il codice dell’algoritmo di base, variando al contempo alcuni passaggi specifici nelle sottoclassi. Il problema affrontato dal pattern Template Method è quello di dover implementare un algoritmo che è per lo più simile in 28 diverse classi, ma presenta alcune variazioni specifiche per ogni classe. Implementare ogni variante dell’algoritmo da zero in ogni classe porterebbe a una grande quantità di codice duplicato. Un esempio potrebbe essere un’applicazione che deve elaborare documenti in diversi formati, come DOC, CSV e PDF. Sebbene ogni formato di file richieda un trattamento specifico, l’algoritmo di base per estrarre e analizzare i dati è simile per tutti i formati. Il pattern Template Method risolve questo problema definendo un ”template method” nella superclasse, che implementa l’algoritmo di base come una sequenza di chiamate a metodi. Questi metodi possono essere di tre tipi: Passi astratti: Metodi senza implementazione nella superclasse, che devono essere imple- mentati dalle sottoclassi. Nel caso dell’applicazione di analisi dei documenti, un esempio di passo astratto potrebbe essere extractData, che si occupa di estrarre i dati grezzi dal file. Passi opzionali: Metodi con un’implementazione predefinita nella superclasse, che pos- sono essere sovrascritti dalle sottoclassi se necessario. Ad esempio, il metodo analyzeData potrebbe avere un’implementazione generica nella superclasse, ma le sottoclassi potrebbero sovrascriverlo per fornire un’analisi più specifica per il tipo di documento. Hook: Metodi opzionali con un corpo vuoto nella superclasse, che offrono alle sottoclassi punti di estensione aggiuntivi per l’algoritmo. Ad esempio, un hook beforeAnalysis potrebbe essere chiamato prima dell’analisi dei dati, consentendo alle sottoclassi di eseguire operazioni preliminari specifiche. Le sottoclassi possono quindi ridefinire i passi astratti e opzionali per personalizzare l’algoritmo per le loro esigenze specifiche, senza dover riscrivere l’intero algoritmo. Il pattern Template Method è particolarmente utile quando si ha a che fare con algoritmi che presentano una struttura generale simile, ma che richiedono variazioni specifiche in alcuni passaggi. È importante progettare correttamente i passi del template method, assicurandosi che siano sufficientemente generici da poter essere riutilizzati in diverse sottoclassi, ma anche abbastanza specifici da consentire la personalizzazione. 1.6.10 Il Pattern Visitor È un design pattern comportamentale che consente di aggiungere nuove operazioni a una gerar- chia di oggetti, senza modificare le classi degli oggetti stessi. Il pattern realizza questo obiettivo introducendo una classe ”visitor” separata, che contiene le operazioni da eseguire sugli oggetti della gerarchia. Il pattern Visitor si rivela utile quando si ha la necessità di eseguire operazioni su oggetti appartenenti a una gerarchia di classi, ma si vuole evitare di aggiungere queste operazioni direttamente nelle classi degli oggetti. Ciò può accadere per diversi motivi, ad esempio: La gerarchia di classi è già complessa e si vuole evitare di appesantirla ulteriormente con nuova logica. Si vuole mantenere la gerarchia di classi stabile e indipendente dalle operazioni specifiche che potrebbero essere eseguite su di essa. Si prevede di aggiungere nuove operazioni in futuro e si vuole evitare di dover modificare le classi degli oggetti ogni volta. 29 Un esempio efficace dell’uso del pattern Visitor può essere lo sviluppo di un’applicazione che gestisce dati geografici in un grafo. Ogni nodo del grafo rappresenta un’entità geografica, come una città o un’industria, e ogni tipo di nodo è rappresentato da una classe specifica. A un certo punto, sorge la necessità di esportare il grafo in XML. Tuttavia, l’architetto del sistema si oppone all’idea di modificare le classi dei nodi esistenti per aggiungere il codice di esportazione, sia per non rischiare di introdurre bug in un codice già in produzione, sia perché l’esportazione XML è una funzionalità separata dalla gestione dei dati geografici. Il pattern Visitor propone di creare una classe ”visitor” separata, che contiene i metodi per eseguire le operazioni desiderate sugli oggetti della gerarchia. Ogni metodo del visitor è specifico per un tipo di oggetto della gerarchia e riceve come parametro un’istanza di quel tipo di oggetto. Nell’esempio del grafo geografico, la classe ExportVisitor avrebbe un metodo doForCity per es- portare gli oggetti di tipo City, un metodo doForIndustry per esportare gli oggetti di tipo Industry e cosı̀ via. Per consentire ai visitor di accedere agli oggetti della gerarchia, il pattern Visitor prevede l’utilizzo di un meccanismo chiamato double dispatch. In pratica, ogni classe della gerarchia im- plementa un metodo accept, che riceve come parametro un visitor. Il metodo accept chiama il metodo del visitor specifico per il tipo di oggetto corrente, passando se stesso come parametro. Ad esempio, la classe City implementerebbe il metodo accept come segue: class City is method accept(Visitor v) is v.doForCity(this) In questo modo, quando si vuole eseguire un’operazione su un oggetto della gerarchia, si crea un’istanza del visitor appropriato e si chiama il metodo accept dell’oggetto, passando il visitor come parametro. Il metodo accept si occuperà di chiamare il metodo corretto del visitor, a seconda del tipo di oggetto corrente. Il pattern Visitor può risultare complesso da implementare, soprattutto se la gerarchia di classi è molto articolata. Inoltre, l’aggiunta di nuovi tipi di oggetti alla gerarchia richiede di aggiornare tutti i visitor esistenti, il che può comportare un certo sforzo di manutenzione. Tuttavia, i vantaggi offerti dal pattern Visitor in termini di flessibilità, riusabilità e manutenibilità del codice spesso superano questi svantaggi. È importante notare che il pattern Visitor è simile al pattern Command, ma ci sono alcune differenze chiave. Il pattern Command incapsula una richiesta come un oggetto, consentendo di parametrizzare i client con diverse richieste, di accodare o registrare le richieste e di supportare operazioni annullabili. Il pattern Visitor, d’altra parte, si concentra sull’aggiunta di nuove operazioni a una gerarchia di oggetti esistente, senza modificare le classi degli oggetti stessi. 30 2 Ciclo di Vita di un Progetto Il concetto di ciclo di vita di un progetto software racchiude tutte le attività e le azioni necessarie per realizzare un progetto software, dalla sua concezione fino al suo ritiro. Scegliere il ciclo di vita appropriato è fondamentale per il successo del progetto, in quanto fornisce un framework per la pianificazione, l’organizzazione e il controllo delle attività di sviluppo. Nel corso degli anni, l’ingegneria del software ha visto la nascita di diversi modelli di ciclo di vita, ognuno con i suoi pro e contro. Ci sono due categorie principali per la realizzazione di un progetto software: Modelli tradizionali: Questi modelli, come suggerisce il nome, sono stati i primi ad essere utilizzati e sono caratterizzati da un approccio sequenziale allo sviluppo. Tra questi troviamo il modello a cascata (waterfall), il modello a cascata con ritorno e il modello incrementale. Questi modelli offrono una struttura semplice e facile da gestire, ma possono risultare rigidi e inadatti a progetti complessi o in rapida evoluzione. Metodologie agili: Nate come risposta ai limiti dei modelli tradizionali, le metodologie agili si fondano su principi come l’iteratività, la flessibilità, il coinvolgimento costante del cliente e la centralità della codifica. In dettaglio faremo un focus su metodologie come Extreme Programming (XP), Scrum e Kanban. Queste metodologie si adattano meglio ai progetti con requisiti in evoluzione, promuovono la comunicazione e la collaborazione tra team di sviluppo e stakeholder e consentono di rilasciare nuove versioni del software con maggiore frequenza. La scelta del modello di ciclo di vita più adatto dipende da diversi fattori, tra cui la complessità del progetto, le dimensioni del team di sviluppo, le esigenze del cliente e il livello di rischio accettabile. 2.1 I Modelli Tradizionali I modelli tradizionali di ciclo di vita del software, spesso indicati come modelli a cascata (waterfall), sono stati i primi ad essere utilizzati nello sviluppo del software. Si distinguono per un approccio sequenziale e lineare, dove ogni fase deve essere completata prima di passare a quella successiva. Le fonti menzionano quattro modelli tradizionali: modello a cascata, modello a cascata con ritorno, modello incrementale e modello a spirale. 2.1.1 Il Ciclo di Vita a Cascata Il modello a cascata, noto anche come waterfall, è stato il primo modello di ciclo di vita del software ad essere sviluppato e, di conseguenza, è ancora oggi il più conosciuto e diffuso. Questo modello si basa su un approccio sequenziale e lineare, in cui lo sviluppo procede attraverso una serie di fasi distinte, ognuna delle quali deve essere completata prima di passare alla successiva. Ecco le fasi tipiche del modello a cascata: Studio di fattibilità: Questa fase iniziale si concentra sulla valutazione della fattibilità del progetto, sia dal punto di vista tecnico che economico. È fondamentale in questa fase un’intensa interazione tra progettisti e committenti per definire il problema in modo rigoroso e valutare le diverse alternative di implementazione. La formalizzazione del problema dovrebbe essere sufficientemente rigorosa, ma non eccessivamente complessa. 31 Raccolta e analisi dei requisiti: In questa fase, i requisiti del software vengono raccolti, analizzati e documentati in modo dettagliato. I requisiti possono essere funzionali, non fun- zionali, tecnologici o inversi. Per semplificare questa fase, è utile suddividere il progetto in moduli di dimensioni minori. Progettazione: Durante la fase di progettazione, i requisiti raccolti vengono tradotti in una specifica tecnica del software. Questa specifica, spesso documentata tramite UML (Unified Modeling Language), definisce l’architettura del sistema, i moduli che lo compongono e le loro interazioni. Codifica: In questa fase, il progetto viene implementato, ovvero viene scritto il codice sor- gente