venerdì 26 febbraio 2021

Behavioral Patterns

Behavioral patterns

Introduzione


I behavioral patterns sono design patterns che si focalizzano su come gli oggetti di un sistema comunicano tra loro e svolgono le proprie funzioni. Si suddividono principalmente in Class Behavioral patterns, che usano l'ereditarietà per distribuire le funzioni alle varie sottoclassi,  ed i Behavioral Object patterns, che invece usano la composizione. Nel secondo caso ci un oggetto conterrà i riferimenti a uno o più
(eventualmente tutti) gli altri oggetti con cui collabora. 

Ne esamineremo i principali riassumendone le caratteristiche ed il funzionamento.


Chain of Responsibility

E' un  Behavioral Object pattern e riguarda la possibilità di inviare dei comandi ad un oggetto ma dando la possibilità di "evadere" quei comandi anche ad altri oggetti.
In questo schema abbiamo in genere una gerarchia di oggetti,  in cui ad un nodo perviene un messaggio, ossia la richiesta di eseguire una certa funzione.
Quel nodo potrebbe essere o meno in grado di eseguire l'incarico, o eventualmente potrebbe svolgerne solo una parte e delegare il resto a altri nodi. 
In una gerarchia, potrebbe delegare tutto o parte dell'incarico al suo nodo "parent". In un generico reticolo a uno o più nodi a lui prossimi.
La gerarchia potrebbe essere preesistente al meccanismo di chain of responsibility, oppure creata appositamente se non ve n'è già una adeguata.
Una generica implementazione potrebbe prevedere un'interfaccia tipo:
ed una classe minimale che la implementa potrebbe essere:

class Handler:IHandler {
     IHandler parentNode;
     public  Handler(IHandler parent){
         this.parentNode=parent;
     }
     public  void HandleRequest(object input){
        if (può gestire la richiesta rappresentata dall'input) {
              //codice che gestisce la richiesta
        }  
        else {
              if(parentNode != null) { //se c'è un nodo parent
                   parentNode.HandleRequest(input);  //gira la richiesta al parent
              }
         }       
     }
}  

In questo caso la richiesta è eseguita sempre solo da uno dei nodi della gerarchia, ma non è escluso che un nodo possa effettuare delle operazioni e poi girare comunque la richiesta al parent node.

Se nessun nodo gestisce la richiesta, il flusso dell'invocazione risalirà sino al nodo root della gerarchia, ad esempio se la gerarchia è data dai controlli visuali presenti un una maschera, il root sarà la maschera stessa. La root potrebbe ignorare la richiesta o implementare un comportamento generico.

In questo schema c'è un accoppiamento abbastanza ridotto, poiché ogni nodo conserva al massimo un riferimento al proprio parent, c'è il vantaggio che non è necessario conoscere in partenza quale nodo eseguirà effettivamente la funzione richiesta, e però non si ha neanche la certezza che qualcuno la gestirà. Eventualmente la root in questo caso potrebbe visualizzare un messaggio o intraprendere azioni più generiche. In questo caso la classe potrebbe assomigliare a:

class Handler:IHandler {
     IHandler parentNode;
     public  Handler(IHandler parent){
        this.parentNode=parent;
     }
     public  void HandleRequest(object input){
       if (può gestire la richiesta rappresentata dall'input) {
            //codice che gestisce la richiesta
        }  
       else {
            if(parentNode != null) { //se c'è un nodo parent
                parentNode.HandleRequest(input);  //gira la richiesta al parent
            }
              else {
               //Messaggio di errore: comando non eseguito
              }
        }       
     }
}

Dato che i comandi possono essere diversi, si pone il problema di come passare l'input ai vari comandi. Un metodo può essere avere un oggetto di tipo IRequest:

interface IRequest {
    CommandKind getKind();
    object Data;
}

e poi nel metodo HandlerRequest(IRequest data) esaminare il kind del parametro per stabilire il tipo effettivo di dati presenti nel campo Irequest.Data, ad esempio supponiamo di voler processare con un certo nodo solo le richieste di "tipo 1":

  public  void HandleRequest( IRequest data ){
       if (data.getKind()=="tipo 1") {
            //codice che gestisce la richiesta
              TDato1 d1 = (TDato1) data.Data;
              //codice che opera su d1...
              return;
        }  
        if(parentNode != null) { //se c'è un nodo parent
             parentNode.HandleRequest(data);  //gira la richiesta al parent
       }         
  }


Command

Rappresenta una richiesta di esecuzione di una funzione in un oggetto in modo che possa essere eseguito in una coda o possa esserne effettuato il rollback.
L'informazione memorizzata include sia i parametri di invocazione, che il riferimento dell'oggetto su cui deve essere eseguita, e anche il nome (o qualsivoglia discrimine) della funzione da eseguire.
Se si vuole poter effettuare il rollback, potrebbe essere necessario anche memorizzare tutto o parte dello stato dell'oggetto da invocare al momento dell'esecuzione della funzione.
Una volta che si è incapsulato in un oggetto tutti i dati necessari ad invocare una funzione, si ha il vantaggio di poterlo trattare come un qualsiasi dato e quindi per esempio:
  • creare macrocomandi, ossia elenchi di comandi da eseguire in sequenza
  • consentire di salvare un log delle azioni effettuate
  • consentire l'"undo" di una o più operazioni ove nell'oggetto vi siano anche le informazioni sull'oggetto al momento dell'applicazione della funzione
  • consentire il "redo" ossia applicare di nuovo una operazione
  • come conseguenza della precedente, consentire le "transazioni", ossia invertire le prime n azioni di una sequenza ove la (n+1) generi un errore, in sostanza consentire di eseguire "tutto il blocco o niente"
In questo pattern distinguiamo:
  • un'interfaccia ICommand implementata da tutti i comandi
  • delle classi Comando_xxx che implementano l'interfaccia ICommand
  • un client che crea i Comandi Comando_xxx  e stabilisce su quale oggetto applicarli
  • l'invocatore che richiede al comando di agire
  • il Receiver, tipicamente  un documento o un'applicazione, su cui applicare i comandi
Possiamo osservare che il Command disaccoppia completamente l'oggetto che richiede l'operazione dall'oggetto su cui è effettivamente applicata, infatti l'invocatore richiama un metodo di ICommand e la classe che ne implementa l'interfaccia eseguirà un metodo sul receiver in base ai parametri impostati. Questo potrebbe anche diventare utile ai fini di cambiare l'interfaccia di una classe receiver, o usare un'altra classe concreta Receiver senza dover necessariamente cambiare il client.


Interprete

Data una espressione scritta in un certo linguaggio, ne effettua la valutazione seguendo la grammatica associata al linguaggio stesso.
La grammatica esprime quali sono le unità elementari del linguaggio e come esse si compongono per creare sequenze più complesse. Una semplice grammatica è quella delle espressioni algebriche, ma come noi informatici sappiamo, anche tutti i linguaggi di programmazione possiedono una loro grammatica, senza parlare della grammatica dei linguaggi naturali.
L'interprete associa ad ogni unità elementare un'azione, e ad ogni regola di grammatica altrettante azioni, in modo che da una certa espressione o frase appartenente al linguaggio, questa venga decomposta in una gerarchia di oggetti ad ognuno dei quali possa essere applicata la relativa azione in modo ricorsivo, a partire dalla regola di più alto livello.

Personalmente, mi sono sempre divertito molto ad usare questo pattern, sia quando in gioventù ho scritto un compilatore lisp ed uno pascal, e sia in seguito quando ho scritto un traduttore da filtri in TSQL a c# per poter creare delle query da applicare sulle strutture in memoria, e viceversa trasformare un oggetto "query" c# in stringa per poterlo eseguire su db, usando dialetti diversi a secondo di un driver generico per un db. Non è un pattern di uso frequente, ma ci sono casi in cui è assolutamente l'unica possibilità per risolvere determinati problemi.
Probabilmente la parte più complicata è quella di scrivere il parser che decompone le frasi del linguaggio nella gerarchia di oggetti, ma una volta che si fa la pratica diventa facile, senza considerare che ci sono librerie in circolazione che agevolano questa fase, e che tuttavia non ho mai usato.

In questo pattern distinguiamo:
  • un interprete, che dato un albero derivante da un'espressione è in grado di valutarla
  • espressioni elementari (semplici caratteri o segni di punteggiatura, o sequenze prestabilite)
  • espressioni non elementari date dalla concatenazione delle precedenti secondo determinati criteri
  • un contesto da applicare nella valutazione dell'espressione (ad esempio i valori da attribuire a delle variabili)
  • un client che costruisce l'albero sintattico a partire da una sua sequenza e chiede all'interprete la sua valutazione

Una volta decomposto lo statement in un albero sintattico, la valutazione del risultato sarà la composizione di una serie di oggetti ognuno associato ad un elemento (elementare o composto) della frase stessa, e tipicamente con una classe diversa per ogni elemento delle regole della grammatica considerata. Ad esempio ci potrà essere una classe che effettua l'operazione somma, una l'operazione and logico etc, e ogni oggetto-operazione (elemento composto) conserverà i riferimenti agli oggetti relativi agli operandi (o elementi più elementari) e cosi via.


Strategy pattern

Definisce una famiglia di algoritmi, li incapsula in un insieme di classi, e li rende intercambiabili. Lo strategy pattern rende il client indipendente dall'algoritmo utilizzato. 
E' possibile utilizzare più strategy pattern in composizione per creare una logica applicativa dinamica e configurabile.
Usare lo strategy pattern aiuta a non creare tante classi simili e che differiscono solo per un comportamento. E' vero che creando una gerarchia di classi si potrebbe ottenere un effetto simile, ma non sarebbe possibile cambiare in itinere il comportamento, e soprattutto, ove siano presenti più punti di variazione, costringerebbe a creare tante classi derivate quante il prodotto cartesiano di tutte le varianti.
Si potrebbe anche inserire nel codice dei vari metodi delle selezioni per usare un algoritmo (o una logica in generale) o un altro ma l'intersecarsi dei vari algoritmi renderebbe il codice poco leggibile e manutenibile. Separare invece le diverse logiche in oggetti distinti semplifica il codice e anche il suo unit testing.

In questo pattern distinguiamo:
  • IStrategy, l'interfaccia comune alle classi che implementano i diversi algoritmi
  • "ConcreteStrategy" le varie classi che implementano i diversi algoritmi
  • Context, il client in cui è presente un riferimento alla classe specifica che implementa l'interfaccia IStrategy e può eventualmente definire un'interfaccia che consenta a tale classe di accedere ai propri dati in qualche modo.
Fondamentale nel progettare uno strategy pattern è quindi definire un'interfaccia comune per tutte le ConcreteStrategy, che sia abbastanza generica da poter consentire ad ognuna di operare ed esporre la stessa interfaccia, d'altronde non è necessario che tutti i parametri sia effettivamente significativi. Inoltre è bene che IStrategy sia più semplice possibile per non renderne l'uso eccessivamente complicato. E' possibile anche rendere opzionale l'applicazione della ConcreteStrategy, dotando il client di un comportamento di default inizialmente.



Iterator

Anche detto "cursore", fornisce un metodo per scorrere gli elementi di una struttura (lista, albero, grafo..) senza conoscerne i dettagli di implementazione.
Tipicamente una classe iterator espone un'interfaccia con i metodi:
  • first(), vai al primo elemento
  • next(), vai al prossimo elemento
  • hasMore(), verifica se ci sono altri elementi successivi
  • current(), restituisce l'elemento corrente
In questo pattern distinguiamo:
  • una classe Struttura composta di più elementi
  • una classe Iteratore usato per scorrere gli elementi della struttura
  • una classe Client che possiede un riferimento all'iteratore per operare sugli elementi della struttura

Il separare l'oggetto iteratore dalla struttura stessa consente di poter cambiare l'implementazione della struttura senza inficiare le funzioni che operano iterando su essa. Tali funzioni possono anche non ricevere un riferimento alla struttura, ma direttamente un riferimento ad un suo iteratore.
Tuttavia è possibile anche che una classe rappresentante una struttura implementi essa stessa l'interfaccia di un iteratore, in questo caso parliamo di iteratore "interno" anziché dell'iteratore "esterno" che si ha ove questo risieda in una classe separata.
Poiché per attraversare una struttura è necessario conoscerne i dettagli, e si vuole evitare in generale che nella classe iteratore si inseriscano i dettagli implementativi della classe "struttura", è possibile che la classe "struttura" esponga dei metodi di attraversamento e che nell'iteratore sia usato per memorizzare lo stato di attraversamento, e per il resto richiami i metodi esposti dalla struttura. Questo è quel che accade di solito se la struttura è complessa ed è gestita da una classe "Composite" (vedasi structural patterns)

Un altro uso degli iterator si ha nel caso in cui si voglia accedere agli elementi di una struttura in modi diversi, ossia seguendo diversi ordinamenti. In questo caso, è possibile creare un oggetto iteratore separatamente, con la logica di ordinamento/attraversamento desiderata, e poi utilizzarlo in una funzione che non dipenda più da quella scelta.


Mediator

Definisce un oggetto che incapsula la logica di interazione tra un insieme di altri oggetti. 
Ha senso quando un insieme di oggetti  interagisce con meccanismi complessi ma al contempo ben definiti. La logica di interazione viene centralizzata in un unico oggetto, il mediatore, semplificando il modello di interazione "molti a molti" con un modello "uno a molti" in cui ogni oggetto del sistema comunica solo col mediator.
Il mediator quindi migliora il disaccoppiamento (decoupling)  tra le classi, e sposta la logica di interazione in un unica classe.
Altri vantaggi che si ottengono sono:
  • poter cambiare la logica di interazione senza derivare nuove classi per ogni componente del sistema, infatti basterà cambiare (derivare) solo la classe mediator a tal fine
  • astrarre e rendere quindi più "leggibile" la logica di interazione

Tuttavia centralizzare la logica di controllo rappresenta anche un costo da pagare, poiché è facile che la classe mediator diventi molto complessa e difficile da manutenere.
Classi mediator si trovano spesso nei framework di sviluppo, in cui si promuovono dei modelli di interazione, che agevolano la scrittura di componenti che sono poi inserite in un contesto globale in maniera relativamente semplice, dovendo comunicare solo col mediator con interfacce ben definite e collaudate.


Memento

Serve a catturare lo stato interno di un oggetto, senza violare i paradigmi di information-hiding, così che l'oggetto possa essere serializzato e deserializzato.
Il memento è utile anche per implementare meccanismi di "undo" in una sequenza di operazioni (vedasi il pattern Command ), ad esempio richiedendo un oggetto Memento ad ogni destinatario di un'operazione in modo che volendo tornare allo stato precedente, basterà inviare all'oggetto lo stato (precedentemente ottenuto) da assumere.
Ai fini dell'implementazione del memento, è dunque necessaria una classe che implementi  un'interfaccia tipo:
  interface IMemento {
    object getState();
    void setState(object state);
}

Mentre le classi "origine" di cui si vuole persistere lo stato esporranno un'interfaccia di tipo:

interface ISerializable {
    void setMemento(IMemento m);
    IMemento getMemento();
}


In questo modo l'oggetto che usa le classi "origine" attraverso i metodi getMemento e setMemento potrà impostarne lo stato senza conoscere i dettagli di come questo sia stato codificato.



Observer

Definisce una dipendenza uno a molti tra un oggetto "osservato" ed uno o più "osservatori" in modo tale che ad ogni modifica dell'oggetto osservato, gli osservatori ricevano una notifica automatica.
Si usa quando la modifica di un oggetto "osservato" deve avere degli effetti collaterali su altri oggetti che però devono essere trasparenti a chi effettua la modifica, ossia è attuato un decoupling tra chi agisce sull'oggetto per modificarlo e gli n "osservatori" che attueranno delle azioni in base alle modifiche.

E' ancora più utile se il numero degli osservatori può cambiare dinamicamente durante l'esecuzione. Ogni osservatore decide in autonomia di "registrarsi" o "deregistrarsi" agli eventi dell'oggetto osservato, e riceverà le notifiche (tipicamente tramite una callback oppure esponendo un'interfaccia IUpdate()) ogni volta che ce ne sarà bisogno. 
L'Observer è un caso particolare dell'Inversion of Control, che agisce su ogni modifica dell'observed.
Le componenti di questo pattern sono:
Un oggetto "Observed" che espone l'interfaccia

  interface IObserved {
    object addObserver(IObserver o);
    void removeObserver(IObserver o);
}


Uno o più oggetti "observer" che espongono l'interfaccia 

  interface IObserver {
    void update(object o);
}

e l'oggetto osservato tiene traccia di tutti i propri osservatori ed ogni qualvolta è necessario invoca il metodo update() degli observer passando come parametro se stesso.

E' possibile adottare delle varianti per cui l'observer può decidere di registrarsi solo a determinate modifiche oppure nell'invocazione del metodo update si può specificare in qualche modo cosa sia cambiato, per facilitare il compito dell'observer.
Il prezzo da pagare è che una modifica apparentemente "innocua" all'oggetto observed può avere delle conseguenze anche di rilievo sugli observer, ed eventuali errori risultanti possono essere difficili da individuare.



State

Consente di cambiare comportamento di un oggetto in base al proprio stato interno, in sostanza il cambiamento di stato fa si che l'oggetto con cui si interagisce sia effettivamente diverso e anche appartenente ad una diversa classe.
Questo schema prevede un'interfaccia "IContext" che espone un insieme di metodi comuni a tutte le classi concrete considerate.

interface IContext {
    void method1(..);
    void method2(..);
    ..
    void methodN(..);    
}

C'è una classe Context che espone quest'interfaccia e conserva il riferimento "service" ad una classe concreta a cui "gira" le richieste di invocazione di tali metodi. In base al contesto, l'effettiva istanza a cui punta service può cambiare, e quindi anche l'implementazione dei suddetti metodi. 
Un esempio potrebbe essere:


class Context: IContext  {
   private: 
    IContext service;
   public
    void method1(...){
        service.method1(...);
    }
    void method2(...) {
        service.method2(...);
    }
    ..
    void methodN(...){
        service.methodN(...);
    }
}


class concreteService1 : IContext {
    void method1(..);
    void method2(..);
    ..
    void methodN(..);    
}


interface concreteService2 : IContext {
    void method1(..);
    void method2(..);
    ..
    void methodN(..);    
}


in questo caso a seconda dello stato service sarà di tipo concreteService1 o concreteService2 e di conseguenza il comportamento della classe "Context" sarà completamente diverso.
Questo schema fa si che invece di usare oggetti diversi nel flusso di esecuzione, si possa usare un unico oggetto, il "Context" che però in base al risultato delle operazioni assuma comportamenti diversi, consentendo maggiore semplicità di utilizzo. 
La transizione tra gli stati è gestita (ed è quindi ben localizzata) dall'oggetto "Context".

 


martedì 16 febbraio 2021

Structural Patterns

Pattern strutturali

Ci sono vari modi conosciuti in letteratura in cui classi o oggetti possono combinarsi per ottenere nuovi oggetti, sia a livello statico/strutturale ossia a inerente le classi, che dinamicamente, a livello di istanza/oggetto. Ne presenterò i principali, sintetizzandone le caratteristiche.
 

Adapter 

Anche detto wrapper, modifica l'interfaccia di una classe per renderla compatibile con l'interfaccia richiesta da un client esistente. 
In questo pattern distinguiamo: 
  • un client A, una classe o funzione che  utilizza un servizio avente una certa interfaccia IService
  • una classe di servizi definita da una interfaccia IService
  • un servizio B esistente "da adattare", che espone una interfaccia diversa da IService
  • una classe "Adapter" che espone l'interfaccia IService utilizzando un oggetto di tipo B (in questo caso parliamo di object adapter) o derivando dalla classe B (in questo caso parliamo di class adapter)
Abbiamo dunque due tipi di adapter a seconda che utilizzino l'ereditarietà (class adapter) o la composizione (object adapter). La classe adapter può anche servire ad isolare, per il futuro, eventuali cambiamenti dell'interfaccia di B, per esempio perché sviluppata da terze parti. In tal modo l'applicazione client utilizzerà sempre l'interfaccia esposta dall'adapter a prescindere dal rilascio di nuove versioni di B.
E' da notare che l'interfaccia dell'adapter, pur differenziandosi da quella di B, non varia radicalmente le funzioni svolte, altrimenti non si tratta più di un adapter ma di un altro dei pattern sottoelencati. 
Può anche servire a semplificare l'interfaccia di un servizio molto complesso, magari in vista di un suo completo refactoring.
Un'ulteriore funzionalità che si può realizzare con un object adapter è la possibilità di collegarlo dinamicamente a diversi oggetti (non solo di tipo B) in modo tale che il client possa vederli con un'interfaccia omogenea.
L'adapter può anche servire a modificare il tipo di parametri, per esempio con delle conversioni di valuta o di unità di misura dai parametri in input a quelli effettivamente passati al servizio, così come convertire il tipo del risultato.

Bridge

Serve a separare nettamente l'astrazione/interfaccia di un servizio/classe dalla sua implementazione, così che sia possibile cambiare l'implementazione, o usarne diverse, eventualmente anche cambiandole a run-time, senza cambiare il codice dei client.

Inizialmente ci si potrebbe chiedere come mai dovrebbe esserci bisogno di un pattern apposito per ottenere quello che è già disponibile nella maggior parte dei linguaggi ad oggetti nello strumento delle "interfacce" (interfaces e via dicendo) delle classi. Può capitare perché ad esempio il servizio utilizzato è fornito da una terza parte, oppure perché fa parte di una libreria esistente che non è stata progettata in termini di interfaccia/classe che la implementa.
Ecco che può divenire molto utile stabilire a posteriori un'interfaccia, magari inizialmente identica a quella della classe esistente, o leggermente diversa (un adapter può aiutare). Ancor più se si tratta di una gerarchia di classi, potrebbe essere utile creare in corrispondenza una gerarchia di bridge, raggruppando ove possibile le classi che espongono una stessa interfaccia, ottenendo alla fine di poter operare con un numero inferiore di interfacce rispetto alle classi presenti nel sistema.
Ma non è l'unico caso in cui può servire un bridge. Supponiamo di avere una gerarchia in cui ci sono diverse classi con funzioni simili e vogliamo aggiungere una certa funzionalità ad ognuna di esse, ma non vogliamo raddoppiare il loro numero per creare un classe derivata da ognuna.

In questo pattern distinguiamo:
  • un'interfaccia astratta I
  • un'interfaccia I2 derivata da quella astratta
  • un'interfaccia IC (implementor) che le classi concrete implementanti devono esporre, e tipicamente sono funzioni "primitive", di livello più basso rispetto ad I
  • le classi (concrete implementor) che espongono l'interfaccia IC
La classe "Bridge" utilizza le classi implementanti con l'interfaccia uniforme IC,(un adapter può servire allo scopo). La dipendenza dei client sarà sulla classe bridge anziché sulle classi concrete. Questo può anche essere utile a rendere i client indipendenti dalla piattaforma, i dettagli sulle classi concrete sono completamente nascosti dal bridge, ed il bridge può essere inoltre usato come classe base per creare ulteriori classi.

Composite

Serve a comporre oggetti in strutture gerarchiche che vengono viste dai client come un unico servizio, e gli oggetti in esso contenuti possono essere manipolati in modo omogeneo.
Nella gerarchia distinguiamo i nodi composti, che contengono altri nodi, e i nodi foglia. La gerarchia può includere classi diverse, purché espongano l'interfaccia IComponent.
In questo pattern distinguiamo:
  • un client, che manipola gli oggetti tramite l'interfaccia IComponent
  • l'interfaccia IComponent, un elenco di metodi che si applicano all'intera struttura
  • Composite: è il comportamento dei metodi per i nodi non foglia
  • Leaf: è il comportamenti dei metodi per i nodi foglia
La composizione rende il client molto semplice nel gestire il gruppo di oggetti che implementano l'interfaccia IComponent, potendo agire, con semplici comandi, su tutti gli oggetti del gruppo.
Rende semplice aggiungere nuovi tipi di componenti, a patto che implementino la stessa interfaccia.
Rende possibile gestire oggetti di tipo diverso con un'interfaccia comune, al prezzo però di avere dei metodi comuni e quindi non poter usare tutti i possibili metodi, più specifici, che sarebbero disponibili sulle classi concrete. Per accedere a quelli, è necessario operare dei check a run time sui tipi effettivi o meccanismi simili.


Decorator

Anche detto wrapper, serve ad aggiungere nuove funzionalità ad oggetti esistenti.
Nella programmazione ad oggetti, il modo classico per aggiungere funzionalità ad una classe esistente è di crearne una sottoclasse. Questo però a volte può essere poco pratico, per esempio perché vogliamo poter aggiungere diversi set di funzionalità, o vogliamo avere la possibilità di revocarle.
Questo pattern è anche detto wrapper perché per modificare le funzioni dell'oggetto originale, ne mantiene un riferimento e le richiama a sua volta, "avvolgendole", o "decorandole" con altri comportamenti, ad esempio effettuando elaborazioni aggiuntive prima o dopo l'invocazione delle funzionalità originali.
In questo pattern distinguiamo:
  • un'interfaccia IComponent, dell'oggetto a cui verranno aggiunte le funzionalità
  • un oggetto / classe Component, a cui verranno aggiunte le funzionalità
  • la classe Decorator, che ha la stessa interfaccia IComponent.
  • una o più classi che derivano da Decorator e aggiungono funzionalità a quelle di Component
La classe Decorator non fa altro che mantenere un riferimento all'oggetto Component e in maniera trasparente invoca i metodi di Component e ne restituisce l'output.
L'aggiunta di funzionalità avviene nelle classi che derivano da Decorator, in cui i metodi della classe base sono richiamati ed arricchiti di altre funzionalità.
Ad esempio supponiamo di voler "decorare" una classe 

interface IBanca {
    TransactionResult doTransaction(Transaction t);
}

class Banca:IBanca {
    public TransactionResult doTransaction(Transaction t){
        ..
    }
}


e supponiamo di voler aggiungere un'attività di log ad ogni transazione effettuata.
Andremo a definire una classe BancaDecorator del tipo:

class BancaDecorator:IBanca {
   IBanca banca;
   public BancaDecorator(IBanca banca){
    this.banca=banca;
   }

    public TransactionResult doTransaction(Transaction t){
        return banca.doTransaction(t);
    }
}


A questo punto possiamo definire la nostra classe BancaLogged:
class BancaLogged:BancaDecorator {
  Logger log;
   public BancaLogged(IBanca banca):base(banca){
    this.log = Logger.getSingleton(); // supponiamo ci sia un solo logger nel sistema
   }

    public TransactionResult doTransaction(Transaction t){
        TransactionResult res = base.doTransaction(t);
        log.logOperation(t,res);
        return res;
    }
}

E' possibile effettuare la composizione di più Decorator, poiché espongono tutti la stessa interfaccia e quindi ognuno può essere costruito a partire da un altro Decorator. Componendo più decorator è facile arricchire, anche dinamicamente, una classe di più funzionalità, ad esempio potremmo eseguire una serie di :
    IComponent component = new Component(); //per semplicità
    if (featureA_is_requested) component = new Decorator_A(component);
    if (featureB_is_requested) component = new Decorator_B(component);
    ...
e poi lavorare con la component arricchita/decorata di tutte le funzionalità richieste in modo dinamico.
Se abbiamo N funzionalità richieste, ci basterà scrivere N classi Decorator e non dovremo derivare 2^N sottoclassi per ogni combinazione distinta. Inoltre ogni singolo Decorator si occupa di una singola funzionalità quindi è semplice da scrivere e manutenere.


Facade

Serve a fornire un'interfaccia uniforme e più semplice (da cui il termine Facade o "facciata") ad un insieme di classi/componenti presenti nel sistema il cui funzionamento è complesso.
I metodi esposti dalla classe Facade sono di livello più alto rispetto a quelli delle classi gestite, e consentono di nascondere, all'esterno, l'interazioni con le componenti del sistema gestito.
La Facade non solo semplifica l'uso delle componenti, ma allo stesso tempo disaccoppia i client dai dettagli delle componenti richieste per eseguire effettivamente le azioni invocate. La Facade nasconde tutti i dettagli delle componenti che vengono quindi nascosti all'esterno.
Potremmo avere ad esempio, un sistema composto da
  • Client, l'utilizzatore del servizio
  • FacadeAutomobile, la classe che gestisce i comandi di un'automobile
  • classe MotorinoAvviamento
  • classe Batteria
  • classe Airbag
  • classe SensoriCarrozzeria
  • ..
ed un metodo della classe FacadeAutomobile "avvio" che si occupa di controllare se la Batteria è carica con un metodo della classe Batteria, di verificare lo stato degli Airbag invocando un metodo di quel componente, verificare la chiusura delle portiere usando l'oggetto SensoriCarrozzeria e dando corrente al motorino di avviamento inviando un messaggio alla classe che lo gestisce.
In sostanza con un solo comando "avvia" il client avvia l'automobile, ignorando tutti i dettagli su quali sensori vanno verificati e quali componenti vanno messe sotto tensione e via dicendo.
Il comando "avvia" quindi è un metodo esposto dall'interfaccia FacadeAutomobile, presumibilmente insieme ad altri, ognuno dei quali interagirà in modo, anche complesso, con le componenti dell'automobile, con modalità semplici. Tra l'altro questo fa anche si che anche sistemi piuttosto diversi possano avere un'interfaccia omogenea, poiché i dettagli sono nascosti nell'implementazione della Facade. Ad esempio un'automobile diesel o a benzina potranno essere guidate (utilizzate) con la stessa interfaccia.

Proxy

Fornisce un'interfaccia per accedere ad un altro oggetto/servizio. 
I motivi per cui un proxy può risultare utile o necessario sono diversi, ad esempio:
  • (smart reference) l'oggetto/servizio è molto costoso da costruire in termini di tempo o allocazione di banda o memoria, per cui si cerca di ritardare il più possibile la sua creazione delegandola al proxy. Questo concetto l'abbiamo esaminato nel design pattern "lazy instantiation"
  • (smart reference) l'oggetto è una risorsa condivisa tra più client, e va deallocato solo quando non ci sono più client che lo utilizzano
  • (locking check) accertarsi che chi deve accedere all'oggetto condiviso ne ottenga il controllo esclusivo
  • (protection proxy) verificare che chi accede all'oggetto abbia le autorizzazioni a farlo
  • (remote proxy) l'oggetto si trova su una macchina remota ed il proxy si occupa di inviare i messaggi e ricevere le risposte occupandosi delle comunicazioni 

Le entità coinvolte nel Proxy pattern sono:
  • Il Proxy,  che
    • gestisce il riferimento dell'oggetto Service da gestire, che espone una certa interfaccia IService
    • espone l'interfaccia IService ai propri client, cosi che può essere usato in sostituzione della classe Service (o di altre classi di pari interfaccia)
    • controlla l'accesso al servizio Service ed eventualmente di occupa di allocarlo e deallocarlo
  • Il Servizio (astratto), che espone un'interfaccia IService, ed a cui si accede tramite il Proxy
  • Il Servizio concreto, di cui il Proxy mantiene effettivamente un riferimento. Questo può anche coincidere con il servizio "astratto"

FlyWeight

Serve gestire grosse moli di dati in modo efficiente, minimizzandone l'occupazione di memoria.
E' utile quando:
  • sarebbe necessario allocare un elevato numero di oggetti uguali, ma con il flyweight si riesce a condividere le stesse istanze tra più client e possibilmente senza creare duplicati
  • è possibile sostituire gruppi di oggetti con un singolo oggetto dopo averne separato lo stato "esterno" in un'area distinta, e facendo in modo tale che lo stato "interno" sia condiviso tra tutte le istanze virtuali
Per l'applicabilità è necessario che l'applicazione non abbia bisogno di confrontare gli indirizzi di due oggetti gestiti con il flyweight, perché si avrebbe un'uguaglianza indesiderata

Le entità coinvolte sono:
  • L'interfaccia  IFlyweight, con cui viene elaborato lo "stato esterno" dell'oggetto, ossia la parte non condivisa tra le varie istanze
  • La classe Flyweight, che implementa l'interfaccia IFlyweight. Lo stato di questa classe è condiviso tra tutti i client, ossia deve essere indipedente dall'oggetto condiviso
  • FlyweightFactory, la classe che crea e gestisce gli oggetti  FlyWeight
  • Client, le classi che istanziano e mantengono dei riferimenti agli oggetti FlyWeight, e calcolano e memorizzano lo stato esterno dei flyweight (la parte di stato non condiviso)

Possiamo considerare l'uso del Flyweight quando la parte di stato "esterno" di una classe è relativamente piccolo rispetto alla parte del suo stato "interno", quello condiviso tra tutte le sue istanze. Possiamo ad esempio pensare ad una classe che quando viene inizializzata effettua una serie di calcoli e ne memorizza i risultati in una struttura in memoria. Per esempio, i primi N numeri primi con N molto grande. Ovviamente questa parte non varierà tra le diverse istanze e farà parte dello "stato interno" del flyweight.


Conclusione

Abbiamo esaminato i principali pattern strutturali. Certamente non siamo scesi in dettaglio, ma anche il solo fatto di conoscere la possibilità di utilizzare un certo pattern, quando se ne presenterà l'occasione, ci consentirà di approfondire e studiare meglio i dettagli di quel pattern.
In rete ci sono molte risorse e descrivere l'implementazione di questi pattern avrebbe certamente travalicato gli obiettivi di un post in un blog. 



sabato 13 febbraio 2021

Creational Patterns

Creational patterns

Introduzione

I Creational patterns sono un sottoinsieme dei design patterns che si prefigge di consentire il cambio dei tipi concreti utilizzati nelle classi senza bisogno di dover riscriverne il codice. 
E' possibile che i tipi effettivi da usare siano stabiliti a design time o dinamicamente a run-time, e in quest'ultimo caso non sarà necessaria una ricompilazione.
Quando una classe A crea imperativamente istanze di classi di tipo B, ad esempio per invocarne dei metodi o perché alcuni membri di A sono proprio di tipo B, si crea una dipendenza molto forte di A nei confronti di B, e rende una successiva sostituzione di B con una classe diversa, ad esempio derivata da B, impossibile a meno di cambiare il codice dei metodi di A che creano le istanze di B.
Questa dipendenza palesemente viola il principio OCP (open/closed principle), che afferma che le entità dovrebbero essere aperte alle estensioni, ma chiuse alle modifiche, infatti in questo caso non c'è modo di consentire un'estensione da B a B' se non cambiando il codice di A.
Come se non bastasse, quando si creano gli unit test per i metodi di A, non è possibile in queste condizioni sostituire B con altre classi. Ad esempio potrebbe essere che B richiede l'accesso ad un database o ad un web service o altre risorse di sistema, e questo nell'ambito di uno unit test è impraticabile. 
Esamineremo diversi design patterns che si occupano quindi di evitare di creare questa dipendenza.

Supponiamo che la classe A abbia un metodo che necessita di creare un'istanza di B:
class A {     
   void metodo(){
        B b = new B ();
        ..
    }
}
Vedremo nei vari casi come viene eliminata la dipendenza di A verso B.

Factory Method

Definisce un'interfaccia per creare un oggetto, e lascia che le classi derivate implementino tale interfaccia per stabilire quale classe concreata utilizzare.  E' da notare che invece della classe B in questi casi si usi di solito usare delle interfacce, che dovranno essere esposte sia da B che dalle sue eventuali sostitute. In alternativa si può usare la classe B, magari astratta, e consentire la sostituzione con classi derivate da B.

In questo caso si avrebbe dunque:
abstract class A {     
   abstract B creator();

   void metodo(){
        B b = creator();
        ..
    }
}

La creazione di b è delegata al metodo creator(), che sarà ridefinito nelle classi concrete che deriveranno da A. 
Ci sono diverse varianti di questa tecnica:
  • E' possibile che la classe A non sia astratta e provveda invece un'implementazione di default per  il metodo "creator"
  • E' possibile che il metodo creator restituisca diversi tipi di oggetto. In questo caso prevederà un parametro che le indicherà il tipo di oggetto da creare.
  • E' possibile, per i linguaggi che ammettono i template, che il tipo restituito dal metodo sia specificato nell'invocazione del template
Questa tecnica è molto utile ma diviene complicata se la classe B (o una sua derivata) per essere costruita ha bisogno a sua volta di altre classi. Senza contare il fatto che se la classe A dipende da più classi B,C,D, ogni sua classe derivata dovrà implementare i metodi creator per ognuna di queste.
A tal fine può essere agevolante restituire delle istanze di default nei metodi della classe base A.

Prototype

Utilizza un prototipo di una classe per scegliere la classe utilizzata, e ne invoca un metodo di "copia" o "duplicazione" per crearne nuove istanze.
Questo può agevolare la creazione di classi complicate, e comunque fa si che il tipo concreto da utilizzare sia deciso a run-time.

class A {   
   B b;  
   public A(B prototipo){
    b = prototipo.clone();
   }

   void metodo(){
        //usa l'istanza di B creata nel costruttore come clone del prototipo
        ..operazioni su b
    }
}

Questo può agevolare la creazione di istanze di classe complicate, ad esempio derivanti a loro volta dalla composizione dinamica di altri oggetti, e comunque fa si che il tipo concreto da utilizzare sia deciso a run-time.
La classe/interfaccia B in questo caso deve anche prevedere un metodo clone() che consenta la duplicazione dell'istanza.
Una variante di questa tecnica è l'uso di un prototype manager: il client (la classe A) potrebbe richiedere i prototipi non come parametri costruttore ma ad un servizio specifico, il "manager dei prototipi", che fornirà un metodo che restituisce un prototipo diverso in base a dei parametri specificati e/o ad una configurazione
C'è da dire che implementare il metodo di clonazione può non essere agevole se l'oggetto da copiare ingloba altri oggetti complessi. E' anche vero che in quest'ottica ogni classe implicata fornirà un metodo per clonare se stessa, quindi la clonazione di una classe composta alla fine si riduce alla clonazione dei dati "propri", invocando i metodi di cloning delle classi utilizzate, che innesca una discesa gerarchica.

Singleton

Non è un vero e proprio pattern che crea gli oggetti in realtà, infatti prevede che nell'applicazione esista solo un'istanza di una certa classe. Questo può essere utile per esempio nel caso di una configurazione di sistema, di una classe che interfaccia l'accesso al database, o ad  un web service.
Fatta questa premessa, una tipica implementazione è avere un metodo getInstance che restituisce l'unica istanza della classe, creandola se eventualmente non esiste.
Se nell'esempio di prima B fosse una classe singleton, avremmo qualcosa del tipo:

class A {   
   void metodo(){
        B b = B.getInstance(); //chiede alla classe B la restituzione della sua unica istanza
        ..operazioni su b
    }
}

Varianti del singleton possono prevedere la richiesta di una tra più tipologie di singleton in base ad uno o più parametri del metodo getInstance, e la creazione di "registry" dei singleton, una sorta di lookup che associa ad ogni Type la sua unica istanza.
Personalmente trovo molto comodo il registry dei singleton, che rende anche possibile registrare con molta dinamicità i tipi concreti a quelli astratti, anche in punti diversi del programma, e questo sia in base a condizioni che si possono verificare in esecuzione, che ad un eventuale file di configurazione.
Usando un singleton registry il nostro esempio diverrebbe qualcosa del tipo:

class A {   
   void metodo(){
        B b = registry.getInstance(typeOf(B)) as B; 
        ..operazioni su b
    }
}

Abstract Factory

Fornisce un'interfaccia per creare oggetti di un certo tipo senza specificare esattamente la classe concreta. 
In questo modello l'Abstract Factory è una classe astratta che definisce dei metodi che restituiscono delle classi che implementano delle interfacce. Per essere utilizzata è necessario derivarne una classe concreta che stabilisce quali sono le classi concrete che saranno restituite.

class A {   
   Factory factory;
   public A(Factory f){
    factory=f;
    //in alternativa, se la factory è un singleton, il parametro f è superfluo e possiamo 
    // scrivere:
    factory  = Factory.getInstance();    
   }

   void metodo(){
        B b = f.createB(); 
        ..operazioni su b
   }
}


E' possibile, anche in questo caso, che i metodi di creazione accettino dei parametri per influenzare il tipo concreto da creare, o le sue caratteristiche.
Personalmente ho trovato utile utilizzare una variante dell'abstract factory simile al registry dei singleton, in cui consentire, non con la derivazione, ma mediante la compilazione di un lookup, di associare a dei tipi astratti dei tipi concreti. Ha molto senso per oggetti che non hanno bisogno di parametri per la creazione. In alternativa, devono prevedere dei metodi di inizializzazione comuni nella loro interfaccia.
Utilizzando un registry, singleton, come abstract factory, si avrebbe un codice del tipo:

class A {   
   Factory factory;
   public A(){
    factory  = Factory.getInstance();    
   }

   void metodo(){
        B b = f.create(typeOf(B)) as B; 
        ..operazioni su b
   }
}


Builder

Separa la costruzione di un oggetto dalla sua rappresentazione cosi che lo stesso processo di costruzione possa servire a creare oggetti aventi proprietà diverse. 
Un builder si rende molto utile quando l'oggetto che si intende creare appartiene ad una classe composita, ad esempio una classe A che utilizzi come proprie "componenti" le classi (interfacce) B,C,D. Nell'ipotesi di voler consentire di variare i tipi concreti di B,C,D, e far si che i dettagli di costruzione di A non influenzino il modo in cui il client crea una classe A, avremo una situazione del tipo:

class builderA {   
   Factory factory;
   public builderA (){
   }
   void createB(parametri per la creazione di B){        
   }
   void createC(parametri per la creazione di C){        
   }
   void createD(parametri per la creazione di D){        
   }
   A create(){
   }
}

Dal punto di vista dell'utilizzatore, per la creazione di A ci sarà una sequenza del tipo:

          ///per semplificare uso un costruttore invocato imperativamente
      builderA builder = new builderA();       
      builder.createB(...);
      builder.createC(...);
      builder.createD(...);
      A a = builderA.create();
       
In sostanza il builder conosce i dettagli di come costruire la classe a seconda delle componenti che il client vede come indipendenti tra loro. In alternatia, le varie chiamate createB, createC, createD possono anche riguardare l'impostazione di alcuni aspetti che dovrà avere l'istanza di A che sarà creata restituita dal metodo create() del builder.
Quello che si richiede al metodo create() è che l'oggetto restituito rispetti l'interfaccia A, e non necessariamente sia di un tipo specifico.

Lazy Instantiation

E' una tecnica che prevede, nella creazione di oggetti complessi e compositi, di ritardare la creazione di eventuali parti di essi quando siano effettivamente richieste nell'esecuzione.
Questo ne velocizza l'instanziazione, e può anche evitare del tutto la valorizzazione delle proprietà "lazy" quando in effetti non sono necessarie.
Supponiamo ad esempio che la classe A abbia una proprietà pubblica p che viene calcolata nella sua costruzione ed il cui calcolo sia dispendioso in termini di risorse/tempo necessari.

class A {   
   public P p;
   public A(){
    p = .... calcolo dispendioso;    
   }
}

Secondo questa tecnica è possibile ritardare il calcolo di p alla prima volta che sarà effettivamente necessario, e si può implementare ad esempio con una property get o simili, o in alternativa implementando una funzione che ne restituisca il valore o la calcoli se questo calcolo non è stato ancora effettuato:

class A {   
   private P p;
   P getP(){
    if (p==null) {
        p= calcolo dispendioso...
    }
    return p;
   }

   public A(){    
   }
}

Analogamente nel caso di una property get:

class A {   
   private P _p;
   public P p {get {
                if (_p==null) {
                    _p= calcolo dispendioso...
                }   
              return _p;
              }

   public A(){    
   }
}

Stesso concetto può applicarsi, anche se con una diversa implementazione, nel calcolo di matrici o stutture dati complesse, rendendo il calcolo ogni singola cella o elemento "lazy".Stesso concetto può applicarsi, anche se con una diversa implementazione, nel calcolo di matrici o stutture dati complesse, rendendo il calcolo ogni singola cella o elemento "lazy".

Conclusione

Abbiamo esaminato alcuni dei principali pattern di creazione utilizzabili nell'ambito di un'applicazione di livello enterprise. E' certamente possibile utilizzare combinazioni di tali pattern in base alle necessità, tenendo sempre a mente l'obiettivo di ottenere  un software il più possibile manutenibile ed efficiente.



 

lunedì 8 febbraio 2021

Design Patterns, panoramica

Design Patterns

Introduzione

Uno degli strumenti più potenti nella risoluzione di problemi complessi sono i design patterns, ossia delle soluzioni standard a problemi tipici. 
I principali design patterns
Quando si definisce uno schema di risoluzione e gli si da un nome, si ottengono
numerosi vantaggi, tra i quali: riuscire più facilmente ad inserirlo in un contesto di una problematica più ampia, poter comunicare con altri in maniera efficace potendosi riferire ad un concetto condiviso, e non ultimo, avere una freccia in più nella propria faretra di sviluppatore/progettista!
E' quindi importante acquisire dimestichezza con un numero più grande possibile di design patterns per poterli utilizzare nella risoluzione di problemi complessi e/o nel migliorare soluzioni esistenti.
Un altro vantaggio dei design patterns è che essendo oggetto di letteratura, se ne conoscono ampiamente i vantaggi e gli eventuali svantaggi,  pertanto si sa già in partenza che tipo di risultati si otterranno, anche in termini di leggibilità, manutenibilità, efficienza.

In questa nota faremo una panoramica sui design patterns, introdurremo il concetto di debito tecnico e dei campanelli di allarme (code smells). In successive note saranno spiegati, sinteticamente, alcuni dei principali design patterns

Caratteristiche

Un design pattern si identifica con:
  • un nome
  • la classe di problemi che risolve e/o situazioni in cui è consigliato
  • uno schema di soluzione generale, eventualmente costituito di oggetti o algoritmi
  • vantaggi e svantaggi, in termini di efficienza, occupazione di memoria, manutenibilità etc.

Possiamo suddividere i design patterns in varie classi, tra cui:
  • Patterns creazionali (tra cui: abstract factory, Builder, Factory Method, Prototype, Singleton, la lazy instantiation)
  • Patterns strutturali (tra cui l'adapter, il Bridge, la Composizione, il Decorator, il Facade, il Proxy, il Flyweight)
  • Pattern comportamentali (tra cui la chain of resposibility, il Command, l'Interprete, l'Iteratore, il Mediatore, il Memento, l'Observer, lo State, lo Strategy, il template method, il visitatore
  • Pattern di concorrenza
  • Altri (Naked Objects, Repository, i Layers, il Data Access Object, i Data Transfer Objects, le Reflection, le Pipes, Model View Controller, Model View ViewModel
Le prime tre classi sono presenti nella "bibbia" dei design patterns, ossia "Design Patterns" Elements of Reusable Object-Oriented Software della famosa "gang of four"  Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides

Debito Tecnico

Uno dei vantaggi dei design patterns è quello di contenere o ridurre il debito tecnico (tecnical debt)
Quando si operano modifiche sul codice di un programma complesso, è spesso necessario effettuare una serie di modifiche secondarie su altre parti del programma collegate, inclusa la documentazione, gli unit test, e progressivamente la qualità del codice e delle strutture dati utilizzata degrada, trovandosi ad essere usati in contesti diversi da quelli per cui erano state progettate inizialmente.

Col tempo, se non si gestisce il technical debt, il costo della manutenzione diventa via via più elevato, sino a diventare letteralmente impossibile ed il software non più utilizzabile.

Cause del debito tecnico possono essere:
  • Sviluppo di aggiunte e/o migliorie protratte nel tempo, che rendono le strutture iniziali non più adeguate ai nuovi requisiti
  • Pressioni commerciali sul tempo di consegna, che prevale sulla completezza e accuratezza delle modifiche da fare (inclusa la documentazione)
  • Ritardi nel refactoring: man mano che il debito si accumula è necessario prendere provvedimenti, più si ritardano i provvedimenti e più i costi di refactoring aumentano esponenzialmente
  • Mancanza di comprensione del processo da parte del management, che è spesso cieco al concetto di technical debt e non ne comprende le conseguenze
  • Mancanza di un framework di test
  • Sviluppo del software su  rami paralleli, questo amplifica notevolmente il technical debt, venendo a mancare una visione globale sull'uso delle strutture dati e delle funzioni
  • Mancanza di collaborazione e condivisione della conoscenza tra chi deve lavorare su moduli condivisi e/o comunicanti
  • Ignoranza dei canoni fondamentali sulla scrittura del codice e dei design patterns
L'uso di design patterns, ossia soluzioni note in letteratura ai problemi, senz'altro fa si che le strutture e gli algoritmi utilizzati siano migliori rispetto a soluzioni improvvisate di volta in volta.

Code Smells

I Code Smells sono dei sintomi caratteristici della presenza di debito tecnico e/o altri problemi ancora più gravi.  Sono principalmente violazioni dei principi fondamentali della progettazione del software. I code smells non sono propriamente bug ma debolezze che rendono il codice più difficile da comprendere, più costoso da manutenere ed aumentano il rischio di bug.
Alcuni dei principali sintomi sono:

Sintomi a livello di applicazione

  • Codice duplicato
  • Codice eccessivamente complesso, che potrebbe essere semplificato usando specifici design patterns
  • Necessità di cambiare molte parti di codice a seguito di ogni richiesta di modifica, questo è collegato anche all'accoppiamento tra le classi
  • Effetti collaterali imprevedibili, questi spesso presenti quando non c'è abbastanza information hiding e non è rispettato il principio di single resposibility

Sintomi a livello di classe

  • Classi troppo grandi (le super-classi)
  • Classi che dipendono da dettagli implementativi di altre classi, questo capita se si fanno assunzioni su caratteristiche non documentate di come funziona la classe o servizio utilizzati
  • Classi che fanno troppo poco
  • Complessità ciclomatica elevata: troppo cicli/selettori in una funzione tipicamente indicano che quella funzione andrebbe suddivisa in parti più piccole
  • Eccessivo uso di costanti
  • Presenza di downclassing: classi astratte che vengono "castate" a tipi derivati per esigenze varie
  • Accoppiamento per struttura, quando gruppi di variabili vengono condivise e viaggiano in blocco invece di costituire, ad esempio, un oggetto ben definito

Sintomi a livello di metodo

  • Numero eccessivo di parametri
  • Metodi troppo lunghi. Idealmente una funzione dovrebbe stare in una pagina.
  • Nomi variabile troppo lunghi: sono facili da sbagliare o confondere
  • Nomi variabile troppo corti: sono poco significativi
  • Funzioni che restituiscono dati che nessuno legge
  • Commenti troppo prolissi e poco significativi
  • Righe di codice troppo lunghe: una linea di codice dovrebbe essere leggibile senza scorrere la pagina, e dovrebbe essere comprensibile senza bisogno di rileggerla 10 volte

Quando si rileva un accumulo di technical debt, è necessario operare una manutenzione strutturale o refactoring. Ma ne parleremo nei prossimi articoli, in cui esamineremo anche molti design pattern in dettaglio.




sabato 6 febbraio 2021

Dependency Injection

Cos'è e perché è importante

Come abbiamo visto nell'articolo sull'Inversion of  Control, la dependency injection è una tecnica che si prefigge di rendere indipendenti le classi dagli oggetti che esse utilizzano.

In sintesi, seguendo questo principio, un metodo di una classe non dovrebbe mai direttamente creare oggetti di altre classi. Chiamiamo client la classe utilizzatrice e service la classe utilizzata.

In questi termini, il motivo per cui la D.I. è importante è rendere possibile il cambio del service senza modificare il codice del client, ed è anche molto utile in fase di unit testing perché diventa facile passare degli stub al client ai fini di testarlo. Inoltre rende possibile la separazione delle funzioni, la leggibilità ed il riutilizzo.


Come si implementa

Si distinguono, nella D.I., quattro elementi principali:

  1. il servizio da usare (service)
  2. il client che utilizza il servizio
  3. l'interfaccia che il servizio espone e che viene usata nel client
  4. l'injector, la cui funzione è costruire il servizio e "iniettarlo" nel client
Ci possono essere principalmente tre modalità di injection, e si basano tutte su meccanismi di reflection ossia l'injector analizza il codice della classe da costruire e ne legge i costruttori o le proprietà pubbliche. Se le classi da iniettare richiedono a loro volta altre classi, questo può provocare una costruzione in cascata sempre con lo stesso meccanismo.

Constructor injection

Le dipendenze sono passate nel costruttore della classe dall'injector.
Esempio

public interface B{.. };
public class A {
   B service;
    A (B service) {
      this.service=service;
    }
}
l'injector in questo caso costruirà b di tipo B e chiamerà new A(b) per ottenere un oggetto di tipo A.

Questo tipo di costruzione ha lo svantaggio di richiedere immediatamente tutti i parametri e di non poterli più cambiare in seguito.

Setter injection

Il client espone delle proprietà pubbliche che l'injector valorizza quando crea l'oggetto.

Esempio

public interface B{.. };
public class A {
   B service{get; public set;}
}

l'injector in questo caso creerà una classe di tipo A e ne valorizzerà la proprietà:

var a = new A()
var b = (logica per costruire un oggetto di tipo B)
a.service = b;

Questo tipo di costruzione ha il vantaggio di poter decidere in momenti distinti i servizi da passare ad una classe, e però lascia aperto il problema di come accertarsi che tutte le dipendenze siano state passate prima di utilizzare i metodi del client

Interface injection

Il client espone un'interfaccia mediante la quale l'injector può  valorizzare il servizio.
Esempio
public interface B_Setter{
 public void setB(B service);
}

public class A : B_Setter {
 B service;
 public void setB(B service){
  this.service = service;
 }

}

L'injector in questo caso cercherà un metodo setter (con convenzioni stabilite nell'implementazione dell'injector), ed eseguirà qualcosa tipo:

A a = new A();
B b = (logica per costruire B);
A.setB(b);

Questo tipo di costruzione è utile se nell'assegnare il servizio si desidera anche fare altre operazioni sul servizio stesso, che mal sarebbero riposte nel semplice setter.


Quando utilizzare la dependency injection

Ci sono dei casi in cui è certamente consigliabile utilizzare meccanismi di d.i., e sono dipendenze le dipendenze non stabili, ossia soggette a possibili cambiamenti, come:
  • Oggetti che per essere creati hanno bisogno di una configurazione o che utilizzano risorse di sistema (ad esempio Database). Questi di solito sono anche quelli che creano i maggiori problemi in fase di unit test.
  • Istanze di classi che sono ancora in via di sviluppo o sono sviluppate da altri team, incluse classi presenti in librerie di terze parti
  • Istanze di classi che hanno un comportamento non deterministico, che crea in genere dei problemi nello unit test

Object Composition

Attraverso la Dependency Injection possiamo realizzare il paradigma dell'Object Composition, ossia costruire degli oggetti complessi a partire da  oggetti più semplici, che vengono combinati 
La Object composition può sfruttare la modalità di late-binding presente nella dependency injection ed essere maggiormente efficace, potendo decidere dinamicamente quali saranno le classi da utilizzare.

Gestione del ciclo di vita (Object lifetime)

Avendo il client rinunciato a costruire in autonomia gli oggetti che implementano i servizi, rimane aperto il problema di quando vanno distrutti tali oggetti. Questo deve essere gestito in qualche modo dal framework.

Altri usi

Visto che le classi concrete usate per fornire i servizi sono decise all'esterno del client, si può sfruttare il meccanismo di injection anche per modificare le classi, non solo ai fini del test,  ma per passare altre classi  che forniscano comportamenti aggiuntivi, come applicare il logging, meccanismi di sicurezza, inviare messaggi ad altre classi, o di profiling.
La dependency injection dunque è uno strumento utile anche a realizzare altri design pattern, come vedremo nei futuri articoli.

Refactoring: improving modularization

  Modularità In generale, per modularità si intende la misura in cui un sistema può essere decomposto e ricombinato. La modularizzazione è a...