Uno dei principi più importanti nello sviluppo del software è il principio di progettazione aperto-chiuso. Questo principio di progettazione sottolinea che le classi dovrebbero essere aperte per l'estensione, ma chiuse per la modifica. Il modello di design del decoratore incarna il principio del design aperto-chiuso.
Con il modello di progettazione del decoratore, puoi facilmente estendere una classe assegnandole un nuovo comportamento senza alterarne il codice esistente. Il pattern decoratore lo fa dinamicamente in fase di esecuzione, usando la composizione. Questo modello di progettazione è noto come un'alternativa flessibile all'uso dell'ereditarietà per estendere il comportamento.
Come funziona il modello di design del decoratore?
Sebbene il modello decoratore sia un'alternativa a eredità di classe, incorpora alcuni aspetti dell'ereditarietà nella sua progettazione. Un aspetto chiave del pattern decoratore è che tutte le sue classi sono correlate, direttamente o indirettamente.
Un tipico modello di design decoratore ha la seguente struttura:
Dal diagramma delle classi sopra puoi vedere che il modello decoratore ha quattro classi principali.
Componente: questa è una classe astratta (o interfaccia), che funge da supertipo per il pattern decoratore.
Componente in calcestruzzo: questi sono gli oggetti che puoi decorare con comportamenti diversi in fase di esecuzione. Essi ereditano dall'interfaccia del componente e ne implementano le funzioni astratte.
Decoratore: questa classe è astratta e ha lo stesso supertipo dell'oggetto che andrà a decorare. Nel diagramma delle classi, vedrai due relazioni tra le classi del componente e del decoratore. La prima relazione è ereditaria; ogni decoratore è un componente. Il secondo rapporto è di composizione; ogni decoratore ha un (o esegue il wrapping di un) componente.
Decoratore di calcestruzzo: questi sono i singoli decoratori che danno a un componente un comportamento specifico. Dovresti notare che ogni decoratore concreto ha una variabile di istanza che contiene un riferimento a un componente.
Implementazione del Decorator Design Pattern in Java
Un'applicazione di esempio per l'ordinazione di pizze può dimostrare adeguatamente come utilizzare il pattern decoratore per sviluppare applicazioni. Questa applicazione di pizza di esempio consente ai clienti di ordinare pizze con più condimenti. La prima classe del pattern decoratore è l'interfaccia pizza:
pubblicointerfacciaPizza{
pubblicoastratto Corda descrizione();
pubblicoastrattoDoppiocosto();
}
L'interfaccia Pizza è la classe del componente. Quindi, puoi creare una o più classi concrete da esso. La pizzeria produce due tipi principali di pizze, in base al loro impasto. Un tipo di pizza ha la pasta lievitata:
pubblicoclassePizza in crosta di lievitoimplementaPizza{
@Oltrepassare
pubblico Corda descrizione(){
ritorno"Pasta per pizza fatta con lievito madre";
}
@Oltrepassare
pubblicoDoppiocosto(){
ritorno18.00;
}
}
La YeastCrustPizza è la prima concreta Classe Java dell'interfaccia Pizza. L'altro tipo di pizza disponibile è la focaccia:
pubblicoclasseFocacciaCrustPizzaimplementaPizza{
@Oltrepassare
pubblico Corda descrizione(){
ritorno"Impasto per pizza fatto con focaccia";
}
@Oltrepassare
pubblicoDoppiocosto(){
ritorno15.00;
}
}
La classe FlatbreadCrustPizza è il secondo componente concreto e, come la classe YeastCrustPizza, implementa tutte le funzioni astratte dell'interfaccia Pizza.
I decoratori
La classe decoratore è sempre astratta, quindi non puoi creare una nuova istanza direttamente da essa. Ma è necessario stabilire una relazione tra i diversi decoratori e i componenti che andranno a decorare.
pubblicoastrattoclasseToppingDecoratorimplementaPizza{
pubblico Corda descrizione(){
ritorno"Topping sconosciuto";
}
}
La classe ToppingDecorator rappresenta la classe decoratore in questa applicazione di esempio. Ora l'azienda della pizza può creare molti condimenti (o decoratori) diversi, utilizzando la classe ToppingDecorator. Diciamo che una pizza può avere tre diversi tipi di condimenti, vale a dire formaggio, peperoni e funghi.
Topping al formaggio
pubblicoclasseFormaggioestendeToppingDecorator{
privato Pizzapizza;pubblicoFormaggio(Pizzapizza){
Questo.pizza = pizza;
}@Oltrepassare
pubblico Corda descrizione(){
ritorno pizza.descrizione() + ", Topping al formaggio";
}
@Oltrepassare
pubblicoDoppiocosto(){
ritornoPizza.costo() + 2.50;
}
}
Topping ai Peperoni
pubblicoclassePeperoniestendeToppingDecorator{
privato Pizzapizza;pubblicoPeperoni(Pizzapizza){
Questo.pizza = pizza;
}@Oltrepassare
pubblico Corda descrizione(){
ritorno pizza.descrizione() + ", Peperoni Topping";
}
@Oltrepassare
pubblicoDoppiocosto(){
ritornoPizza.costo() + 3.50;
}
}
Topping ai funghi
pubblicoclasseFungoestendeToppingDecorator{
privato Pizzapizza;pubblicoFungo(Pizzapizza){
Questo.pizza = pizza;
}
@Oltrepassare
pubblico Corda descrizione(){
ritorno pizza.descrizione() + ", Topping ai funghi";
}
@Oltrepassare
pubblicoDoppiocosto(){
ritornoPizza.costo() + 4.50;
}
}
Ora hai una semplice applicazione implementata utilizzando il modello di progettazione del decoratore. Se un cliente dovesse ordinare una pizza in crosta di lievito con formaggio e peperoni, il codice di prova per quello scenario sarà il seguente:
pubblicoclassePrincipale{
pubblicostaticovuotoprincipale(Stringa[] argomenti){
Pizza pizza1 = nuovo Pizza in Crosta di Lievito();
pizza1 = nuovo Peperoni (pizza1);
pizza1 = nuovo Formaggio (pizza1);
System.out.println (pizza1.description() + " $" + pizza1.costo());
}
}
L'esecuzione di questo codice produrrà il seguente output nella console:
Come puoi vedere, l'output indica il tipo di pizza insieme al suo costo totale. La pizza è iniziata come una pizza con crosta di lievito per $ 18,00, ma con il modello decoratore, l'applicazione è stata in grado di aggiungere nuove funzionalità e il loro costo adeguato alla pizza. Quindi, dando alla pizza un nuovo comportamento senza alterare il codice esistente (la pizza in crosta di lievito).
Con il pattern decoratore, puoi anche applicare lo stesso comportamento a un oggetto tutte le volte che vuoi. Se un cliente ordina una pizza con tutto sopra e del formaggio extra, puoi aggiornare la classe principale con il seguente codice per riflettere questo:
Pizzapizza2 = nuovo Pizza in Crosta di Lievito();
pizza2 = nuovo Peperoni (pizza2);
pizza2 = nuovo Formaggio (pizza2);
pizza2 = nuovo Formaggio (pizza2);
pizza2 = nuovo Fungo (pizza2);System.out.println (pizza2.description() + " $" + pizza2.costo());
L'applicazione aggiornata produrrà il seguente output nella console:
I vantaggi dell'utilizzo del modello di progettazione del decoratore
I due principali vantaggi dell'utilizzo del modello di progettazione del decoratore sono la sicurezza e la flessibilità. Il modello decoratore consente di sviluppare un codice più sicuro non interferendo con il codice protetto preesistente. Estende invece il codice esistente attraverso la composizione. Prevenire efficacemente l'introduzione di nuovi bug o effetti collaterali indesiderati.
A causa della composizione, uno sviluppatore ha anche molta flessibilità quando utilizza il modello decoratore. Puoi implementare un nuovo decoratore in qualsiasi momento per aggiungere un nuovo comportamento, senza alterare il codice esistente e interrompere l'applicazione.