Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

DDD semplificato nel mondo reale

Posted by Ricibald on 29th July 2014

Il DDD è un ottimo pattern ma se si implementa al 100% si rischia di ottenere troppa complessità rispetto a domini quasi sempre non così complessi da giustificarne una totale aderenza.

Basti riflettere infatti sul numero di astrazioni introdotte per fare una semplice insert:

  1. Application inizializza lo Unit of work e coordina i vari Domain Service
  2. Domain Service ottiene dal suo Repository il proxy del Aggregate che ha attivo il change tracking. Se necessario esegue logica specifica del caso d’uso che non si rimappa nel dominio
  3. Aggregate coordina il comportamento desiderato delegando le varie sottofunzioni alle varie Entity o Value Object in stile OO
  4. Entity o Value Object alterano il proprio stato, registrato dal change tracking verso lo Unit of work
  5. Application richiede il commit dello Unit of work
  6. Unit of work apre una micro-transazione che riversa su db lo stato alterato tracciato

Domain Service: perché??

Riporto di seguito una frase di Martin Fowler su cui mi trovo d’accordo:

Any logic that’s particular to a single transaction or use-case is normally placed in Transaction Scripts, which are commonly referred to as use-case controllers. Behavior that’s used in more than one use case goes on the domain objects which are called entities.
Although the controller entity approach is a common one, it’s not one that I’ve ever liked much. The use-case controllers, like any Transaction Script tend to encourage duplicate code. My view is that if you decide to use a Domain Model at all you really should go the whole hog and make it dominant

Il Domain Service è sostanzialmente uno use-case controller che incoraggia alla duplicazione di codice. Se veramente un’operazione è specifica di un caso d’uso bisogna comunque sforzarsi di farla entrare a pieno titolo del dominio. In altre parole: il Domain Service non deve esistere…

Repository e Unit of Work: necessari??

Il Repository pattern consente di ottenere dati da un data source layer mimando una collezione in memoria nascondendo la complessità del sottostante accesso ai dati facendo uso internamente di uno o più oggetti DAO e del pattern Query Object. Nel pattern è importante notare che è normalmente assente il concetto di Update: questo viene inferito automaticamente dallo Unit of work grazie al change tracking sulle entità restituite dal Repository.

Il pattern è efficace poiché tratta il db come aspetto secondario e permette in una prima fase di concentrarsi esclusivamente su GUI, business e TDD. Il pattern è però applicabile solo in presenza di framework ORM che gestiscano in modo efficace il change tracking e il relativo Data Mapper.

A volte il Repository pattern non è però applicabile poiché il data source layer sottostante non è un db, bensì un web service REST o SOAP o una lista Sharepoint. Implementare un meccanismo custom di change tracking diventa veramente complesso e rischioso e non è praticabile.

Bisogna dire addio quindi al DDD e tornare al classico Transaction Script? In realtà si possono implementare versioni semplificate del Repository pattern.

Repository con Unit of Work “a grana grossa”

Ciò che rende complesso il change tracking è la granularità fine: ogni singola modifica viene registrata per contribuire in modo puntuale alla query finale.

Si potrebbero ridimensionare le aspettative e semplicemente marcare l’intera Entity o Aggregate come “dirty” e quindi da aggiornare. Questo può essere implementato in modo esplicito o trasparente:

  • (esplicito) Entity esegue ove richiesto il metodo UnitOfWork.Current.SetDirty(this)
    • tramite PostSharp si può implementare in modo automatizzato su tutte le proprietà
    • tramite T4 si possono autogenerare le Entity rimappando da un modello dati (db, xml, …) arricchendo le Entity con i vari SetDirty
  • (trasparente) Repository crea un proxy delle Entity e decora ove richiesto i metodi con UnitOfWork.Current.SetDirty(this)
  • (trasparente) Entity implementa ove richiesto l’interfaccia INotifyPropertyChanged e Repository gestisce gli eventi PropertyChanged registrando la modifica UnitOfWork.Current.SetDirty(item)

Al momento del Commit dello Unit of work questo avrà la lista delle entità da aggiornare. Sarà compito del nostro Unit of work custom di rimappare gli stati “dirty” con i rispettivi metodi nei vari DAO.

Repository senza Unit of Work: update espliciti

L’approccio precedente è una soluzione buona ma richiede persone esperte che sappiano rimappare lo Unit of work con i vari DAO. Un approccio più facilmente comprensibile è quello di annullare del tutto lo Unit of work e aggiungere nel Repository un metodo Update che opera sull’intero aggregato.

Non avendo nessun meccanismo di notifica dei cambiamenti, Application dovrà ricordarsi di invocare Update per riversare le informazioni contenute nelle Entity nel data source corrispondente altrimenti si otterrebbe un’inconsistenza.

Inoltre non avendo uno Unit of work dovremo farci carico della consistenza del blocco di aggiornamenti:

  • se si tratta di un singolo aggiornamento di un Aggregate in un Repository (caso comune) è sufficiente che la transazione sia definita dentro il Repository stesso
  • se si tratta di aggiornamenti di molteplici Aggregate di diversi Repository è necessario aprire una TransactionScope in Application prima di eseguire i vari Update
  • se si tratta di aggiornamenti di molteplici Aggregate con data source eterogenei è invece necessario prevedere un sistema di compensazione implementando il Command Pattern. Si potrebbe anche valutare l’uso del Distributed Transaction Coordinator

Lo svantaggio di questo approccio è che si delega al programmatore garantire che i metodi di update verso il repository siano 1) invocati correttamente 2) implementati correttamente. Per ridurre tale svantaggio si potrebbe trattare un solo singolo update per l’intero aggregato richiamato dall’Aggregate stesso (applicabile finquando salvare l’intero aggregato si mantiene in limiti di complessità accettabili e finquando abbiamo tutte le informazioni su cosa è cambiato).

Quale usare?

Questa tabella non scientifica cerca di riassumere i pro/contro dell’approccio

Repository standard Repository grana grossa Repository con update
Applicabilità Solo db, buon ORM Code gen (PostSharp, T4) Poco refactoring (no XP)
Skill Alto Medio Basso
Investimento Alto Medio Basso
Manutenibilità Alta Media Bassa

Mettiamo insieme i pezzi…

Vediamo come si rimappa il flusso esposto all’inizio con la soluzione “Repository con update” facendo a meno del Domain Service e immaginando il caso in cui nel caso d’uso si debba aggiornare molteplici Aggregate:

  1. Application ottiene dai Repository i vari Aggregate richiesti
  2. Aggregate coordina il comportamento desiderato delegando le varie sottofunzioni alle varie Entity o Value Object in stile OO. Se necessario esegue logica specifica del caso d’uso
  3. Entity o Value Object alterano il proprio stato
  4. Application apre un TransactionScope e richiede a seconda del caso d’uso l’update o add/remove dai Repository
  5. Repository esegue la corrispondente query
  6. Application richiede il commit del TransactionScope e lo chiude

Se invece ci limitiamo a considerare il semplice caso di aggiornamento di un singolo Aggregate il flusso si riduce come segue:

  1. Application ottiene dai Repository i vari Aggregate richiesti
  2. Aggregate coordina il comportamento desiderato delegando le varie sottofunzioni alle varie Entity o Value Object in stile OO. Se necessario esegue logica specifica del caso d’uso
  3. Entity o Value Object alterano il proprio stato
  4. Application richiede a seconda del caso d’uso l’update o add/remove dai Repository
  5. Repository esegue la corrispondente query nella transazione del DAO sottostante
    • Se coinvolge più aggregati si potrebbe sempre creare un unico metodo nel repository Update(AggregateA a, AggregateB b) che aggiorna entrambi gli aggregati senza dover introdurre lo Unit of work o il TransactionScope

Tramite questa strategia si riesce a ottenere il DDD anche in condizioni non ideali (data source eterogenei, ORM non possibili, team non skillato) senza appesantire eccessivamente la gestione, pur pagando in prospettiva in costi di manutenibilità. Questo implica che un modello di programmazione agile come l’XP non è proponibile poiché non è possibile essere aggressivi con il refactoring.

Appendice: esempio di codice

Si riporta un esempio di codice della soluzione “Repository con update” senza unit of work, con un metodo nel Repository che coinvolge più aggregati e che fa uso del Dependency Injection senza interfacce come illustrato nel precedente articolo:

Posted in .net | 5 Comments »

Problemi del Dependency Injection: come migliorarlo?

Posted by Ricibald on 11th October 2013

Le dipendenze dovrebbero essere ridotte per quanto possibile e seguire principi di basso accoppiamento e alta coesione.

Per aiutare questo nasce il pattern Inversion of Control e i vari framework di Dependency Injection, che consentono di mantenere astratte le dipendenze per poter sostituire i vari “blocchetti” a piacimento. Spesso è un prerequisito fondamentale ai test automatizzati unitari.

Problemi dell’IoC (DI)

Problema 1: troppa astrazione

Mantenere un’astrazione troppo elevata non è sempre un bene e potrebbe creare in realtà problemi al codice.
Spesso chi usa framework di DI rende qualunque dipendenza astratta (così stiamo riscrivendo il framework base!), in realtà dovrebbero esserlo solo quelle poche dipendenze di cui sappiamo che ci sarà una sostituibilità (per es. per i test) e per il resto ricordarsi sempre di seguire le buone regole Object Oriented e relativi Design Pattern (Strategy, Decorator, Adapter).

Ricordarsi buone regole di OO e Design Patern significa quindi ad es. implementare il pattern Strategy utilizzando il container IoC poiché altrimenti nel pattern il Context non istanzierebbe lui il ConcreteStrategy, alterando così un pattern consolidato. Il DI serve solo a risolvere le dipendenze non a implementare pattern. Detto in altri termini:

Il DI non sostituisce la buona POO e relativi Design Pattern!

Se si guarda bene, in realtà, il DI richiede che si abbia anche maggiore competenza nella programmazione OO poiché apre le porte a errori grossolani che normalmente verrebbero segnalati dal compilatore. Ad es. i riferimenti circolari non farebbero nemmeno compilare il progetto, ma se in un progetto abbiamo interfacce disaccoppiate tra loro e in un altro progetto le relative implementazioni, in un classico 3-layer potenzialmente il layer infrastruttura potrebbe invocare un metodo del layer presentation (OMG!).

Nota 1: il bisogno reale dell’astrazione

Il DI spinge come abbiamo detto a un’eccessiva astrazione: immagina di voler definire un’interfaccia ILog. Deve essere astratta, quindi indipendente dalla tecnologia e dalla destinazione, giusto? Se questi sono i criteri rischiamo di definire un’interfaccia ILog eccessivamente complessa quando tutto quello che dobbiamo fare è… loggare!

Sei d’accordo con il ragionamento? Concettualmente è valido vero? Ma nel componente ILog dell’esempio in realtà è un ragionamento sbagliato: bisogna fare un discorso più articolato.

Cercando di ricavare una regola, è necessario creare astrazioni (aumentandone quindi la complessità) se:

  • si deve sostituire completamente l’implementazione interna di un componente con un’altra implementazione completamente funzionante
  • si potrebbe sostituire completamente l’implementazione interna di un componente con un’altra implementazione completamente funzionante e tale componente viene invocato in tanti punti

Quindi se ad es. abbiamo un componente SecurityChecker acceduto solo alla login tutto sommato non ha senso astrarlo: andiamo sul concreto e se cambierà implementazione (da DB a LDAP) cambierà anche il contratto esposto dal componente. Richiederà il refactoring in un singolo punto: la login page, ma non sarà un grosso problema e ci saremo risparmiati tanta complessità in più.

Se viceversa abbiamo un componente ILog questo sarà utilizzatissimo in tutti i punti dell’applicazione. Avere un’interfaccia stabile è importantissimo o si pagherà la penalità di un refactoring costosissimo. Per tali componenti avere un’interfaccia astratta è fondamentale.

Se invece abbiamo un componente SecurityCheckerMock o SecurityCheckerStub è vero sì che sostituisce l’implementazione, ma tale sostituzione non ha pretese di essere completa. Per questo si possono evitare astrazioni e costruire un SecurityCheckerMock che semplicemente eredita da SecurityChecker.

Se infine abbiamo un’applicazione multipiattaforma che deve supportare diverse modalità di autenticazione (LDAP, OAuth, DB) allora nel componente SecurityChecker siamo costretti a definire un’interfaccia abbastanza astratta da comprendere le varie implementazioni.

Nota 2: quanta sostituibilità?

La prima regola appena citata nel creare astrazioni:

si deve sostituire completamente l’implementazione interna di un componente

Apre però uno scenario un po’ preoccupante nel mondo del TDD.

Si consideri la seguente classe, che fa internamente uso di WebClient, XmlSerializer e DateTime.Now per ottenere da un’url remota l’ultima Person salvata da una certa data:

public class Application : IApplication {
    public void GetLastUpdatedPerson() {
        WebClient webClient = new WebClient();
        string lastUpdateDate = DateTime.Now.ToString();
        string personXml = webClient.DownloadString("http://api.myapp.com/person?lastUpdateDate=" + lastUpdateDate);

        XmlSerializer xmlSerializer = new XmlSerializer(typeof(Person));
        using (TextReader reader = new StringReader(personXml))
        {
            return (Person)xmlSerializer.Deserialize(reader);
        }
    }
}

Per abilitare il TDD e quindi la testabilità unitaria della classe Application saremmo obbligati a rimpiazzare tutte le dipendenze con relative astrazioni sostituibili:

public class Application : IApplication {
    private IPersonService _networkManager;
    private ISerializationManager _serializationManager;
    private IDateManager _dateManager;

    public Application(INetworkManager networkManager, ISerializationManager serializationManager, IDateManager dateManager) {
        _networkManager = networkManager;
        _serializationManager = serializationManager;
        _dateManager = dateManager;
    }

    public void GetLastUpdatedPerson() {
        string lastUpdateDate = _dateManager.Now.ToString();
        string personXml = _networkManager.DownloadString("http://api.myapp.com/person?lastUpdateDate=" + lastUpdateDate);
        return _serializationManager.Deserialize<Person>();
    }
}

Questo però significa che in proiezione andremo a wrappare l’intero framework con interfacce sostituibili. Niente da dire, è un approccio formalmente corretto ma rischiamo di aggiungere veramente troppo codice per testare solo che la classe Application coordina correttamente le chiamate passando i parametri giusti: bisogna sempre pesare costi (quanto lavoro e complessità aggiungo) con relativi benefici (“quanto” si testa).

La mia opinione è questa: il software è composto da classi, da moduli e da layer. Secondo me gli unit test puri, che vanno a testare in modo isolato le singole classi, vanno ad aggiungere troppo overhead per consentire la sostituibilità effettiva di tutte le dipendenze.

Molto meglio testare in isolamento i singoli moduli o layer: ha quindi senso costruire un’astrazione attorno al layer di Persistence per testare in isolamento la coppia di layer Application e Domain. O ha senso costruire dei test per la nostra personale libreria di deserializzazione che poi verrà utilizzata direttamente, senza interfacce.

Inoltre ci si dimentica che spesso non si ha un reale bisogno di sostituire l’implementazione: basta configurare l’ambiente in modo opportuno. Prima di un test si possono ad esempio impostare realmente chiavi di configurazione, valori di sessione, condizioni a contorno, web service dummy, database da snapshot, orario del sistema operativo, snapshot di virtual machine con stati “interessanti”. Non bisogna esagerare né nella configurazione (si appesantisce preparazione ed esecuzione dei test) né nella sostituzione (si riscrive il framework), è necessario il giusto compromesso, risultato di tanta esperienza e di tanto refactoring!

Problema 2: l’applicazione, andando nel “concreto”… come diavolo funziona??

Ragionare logicamente su un codice le cui relazioni sono implicitamente legate attraverso configurazioni esterne sottopone a indirezioni in più che ostacolano il lavoro quotidiano. È come se il codice che vediamo avesse una dimensione in più, comprensibile solo ispezionando la configurazione delle dipendenze. Facendo una metafora, basta notare che i siti di e-commerce spesso cercano di ottenere un’esperienza di acquisto “one-click” perché aggiungere un ostacolo a tale processo può compromettere l’acquisto stesso. Per il codice è lo stesso: senza un’esperienza “one-click” per navigare il codice (stile CTRL+Click come Resharper o Eclipse) si rischia di ottenere un complessivo peggioramento della produttività.

L’effetto finale è che risulta complicato seguire il giro finale. Spesso l’unico modo che rimane per capire cosa fa praticamente il codice è… eseguirlo!

Se il codice senza DI è uno Spaghetti Code, il codice con DI diventa in questo modo un Ravioli Code: le classi sono sì disaccoppiate, ma per legarle logicamente tra loro bisogna ricostruire il giro, riproducendo a mente lo stesso Spaghetti Code di prima. In pratica, se le dipendenze sono troppe, potremmo arrivare a un raviolo con un ripieno di tanti spaghetti…

Problema 3: troppa frammentazione

Altro problema è che per ottenere un reale disaccoppiamento tramite DI saremo costretti a definire tante piccole interfacce con relative implementazioni. Sebbene questo sia formalmente valido, applicare buone tecniche nei posti sbagliati può far calare la produttività: far confluire più concetti nella stessa singola classe, sebbene diminuisca la coesione, può risultare per certi scenari più produttivo. Si pensi ad es. a una formula matematica complessa: una cosa è leggere la formula in un unico posto, un’altra è distrecarsi tra vari componenti interdipendenti (e astratti!).

Problema 4: chi istanzia le classi?

Se andiamo a leggere un codice tipico che fa uso di framework di DI:

public class Application : IApplication {
    private IPersonService _personService;

    public Application(IPersonService personService) {
        _personService = personService;
    }

    public void AddPerson(Person person) {
        _personService.Add(person);
    }
}

Il costruttore ottiene da qualcuno un’istanza di IPersonService. Prova a chiedere al tuo IDE chi invoca il costruttore. Come dici? Nessuno invoca il costruttore? L’IDE non è infatti in grado di determinare staticamente chi istanzierà IApplication. Questo complica sia la comprensione del giro, sia l’analisi statica del codice.

Problema 5: trappole nel ciclo di vita delle variabili

Grazie al DI è possibile configurare il ciclo di vita delle istanze. Possono essere Singleton, transient (ogni volta ricreate), thread-scope (legate al thread), request-scope (legate alla richiesta web).

Si veda lo snippet di codice precedente. Piccolo quiz: se IPersonService ha ciclo di vita thread-scope (legato al thread), quando si invoca AddPerson la variabile _personService sarà ricreata quando cambia il thread?

La risposta è… dipende dal ciclo di vita di Application:

Application Scope dichiarato _personService Scope effettivo
Transient Thread-Scope
Thread-Scope Thread-Scope
Singleton Singleton

Se Application è Singleton non importa come è configurato lo scope di IPersonService poiché una volta istanziata Application non verrà mai più risolto. Se quindi IPersonService porta con se uno stato (che dovrebbe essere legato al thread) in questo modo tale stato potrebbe essere invece Singleton, causando un effetto collaterale veramente ostico da rilevare.

Problema 6: analisi statica del codice

Se utilizziamo strumenti di analisi statica del codice come Visual Studio Code Metrics, NDepend o Nitriq otteniamo risultati sfalsati che si rileveranno migliori della realtà effettiva. Se ad es. abbiamo:

  • due interfacce: IPresentation e ICrossComponents. La direzione dovrebbe ovviamente essere: IPresentation vede ICrossComponents (mai viceversa)
  • due implementazioni iniettate: Presentation e CrossComponents
  • uno sviluppatore (moooolto junior) nel costruttore di CrossComponents inietta IPresentation
  • otteniamo un riferimento circolare (!)

Nella pratica avremo un riferimento circolare e un codice scritto male. Ma nei risultati dell’analisi questo non emergerebbe: le due classi Presentation e CrossComponents risulterebbero come mai usate e quindi verrebbero escluse dall’analisi mentre le due interfacce IPresentation e ICrossComponents risulterebbero autonome quando in realtà creano un riferimento circolare.

Si noti che lo stesso problema si ha per:

  • la compilazione: avremo molti warning che in realtà non sono veri problemi, portando presto a ignorare qualunque warning
  • il code analysis di Visual Studio genererebbe molti warning che in realtà non lo sono, costringendoci a ignorare tali warning e, alla lunga, a precluderci i benefici del code analysis in generale
  • i progetti di architettura, che hanno il compito di validare la comunicazione tra i layer, non sarebbero in grado di rilevare il codice che non rispetta i vincoli architetturali stabiliti
  • il Full Analysis di Resharper indicherà che molte classi non sono mai usate, rendendo lo strumento meno potente e a volte anche scomodo
  • il test impact analysis basato su analisi statica (sebbene quelli di Microsoft si basino invece da una baseline di test eseguita)
  • il nuovo CodeLens di VS2013 che si basa su analisi statica per offrire popup informative su metodi per mostrare relativi test e utilizzi
  • poiché molti IoC/DI si basano su configurazione dinamica delle dipendenze, il dead code elimination (come Resharper, Telerik, SmartAssembly o MonoLinker) rileverebbero codice non utilizzato che in realtà verrebbe caricato tramite reflection. I tool non sarebbero usabili.
  • poiché molti IoC/DI si basano su configurazione dinamica delle dipendenze, la pratica di obfuscation (tramite DotFuscator o SmartAssembly) rinominerebbero le classi che contengono le implementazioni, le quali non sono poi più ritrovabili in fase di ottenimento della classe dal container IoC/DI

Questi problemi sono tutti presenti se si usa un framework IoC/DI basato su configurazione (es. Unity). Quelli invece basati su compilazione tramite generics (es. Ninject) non soffrono di alcuni di questi problemi poiché il compilatore “sa” che le implementazioni verranno prima o poi utilizzate.

Problema 7: Design by Contract (DbC) non verificabile

Il Design By Contract si basa sull’idea che l’interfaccia debba funzionare correttamente solo se i chiamanti invocano tale interfaccia in modo corretto e che l’interfaccia non debba quindi ridondare la verifica di condizioni che già devono aver verificato i chiamanti. Il DbC è opposta al Defensive Programming in cui l’onere della verifica è invece del servente, non del chiamante.

Erroneamente si tende a pensare che quando si ha un’interfaccia automaticamente si ha un contratto, in realtà non è così. Per avere un contratto devono essere documentate e, se possibile, verificate automaticamente le caratteristiche corrette di input, precondizioni, postcondizioni e invarianti.

Esistono diversi sistemi per abilitare la verifica automatica dei contratti. La soluzione Microsoft per questo si chiama Code Contract e consente sia una verifica dinamica sia una verifica statica, cioè a tempo di compilazione.

Esempio pratico:

public class Main {
    public static void Main() {
        IoC myContainer = GetConfiguredIoC();
        IApplication app = myContainer.Get<IApplication>();
        app.AddPerson(null);
    }
}

public class Application : IApplication {
    private IPersonService _personService;

    public Application(IPersonService personService) {
        _personService = personService;
    }

    public void AddPerson(Person person) {
        Contract.Requires(person != null);
        _personService.Add(person);
    }
}

Guardando il codice, il metodo AddPerson viene invocato passando null e quindi il chiamante sta violando il contratto. Nonostante questo, l’errore non verrà segnalato a tempo di compilazione, poiché in realtà Main staticamente non sa che la classe sarà Application e quindi non può analizzare staticamente le relative precondizioni.

Ora potremmo anche stare a discutere dei limiti dei Code Contract ma fare retorica non risolve i limiti dei framework a cui dobbiamo adeguarci. La lezione da capire è semplicemente che molti tool che aiutano la produttività hanno bisogno di conoscere le dipendenze in modo statico e molti di questi tool sarebbero inevitabilmente penalizzati.

Problema 8: prestazioni

Avere tanti concetti indipendenti significa non doverci legare all’una o l’altra tecnologia. Se vogliamo fare le cose pulite, significa avere ad es. un progetto MyProject.Persistence con le interfacce e progetti con le implementazioni: MyProject.Persistence.InMemory, MyProject.Persistence.MySql, MyProject.Persistence.SqlAzure. Questo poiché se utilizziamo MySql come persistence non vogliamo portarci dietro le dll di Azure.

Il risultato è avere tanti progetti in VS e tante dll relative. Il progetto finale “reale” non avrà le reference a tutte le dll ma solo a quelle effettivamente necessarie (es. Persistence.MySql) mentre il progetto di “test” avrà le reference verso altre dll (es. Persistence.InMemory).

Il problema non è avere tanti progetti ma è la strategia di compilazione di VS (cioè di MSBuild). Il linguaggio C funziona come dovrebbe: ricompila solo le classi effettivamente impattate da una modifica. In linguaggio C, se quindi ho X che usa Y che usa Z e viene modificata l‘implementazione di Z (ma non l’interfaccia) il compilatore ricompila solo Z lasciando inalterato Y che si basa su un’interfaccia non modificata. Se invece viene alterata l’interfaccia di Z ricompila solo Y lasciando inalterato X. Questo significa che se nel kernel linux si va a modificare l’implementazione network.c la compilazione sarà immediata poiché l’interfaccia non è stata alterata. Una compilazione in un secondo, in un progetto di milioni di righe…

Come funziona invece VS? Se siamo nell’ambito dello stesso progetto… buone notizie, VS compila in modo efficiente seguendo (più o meno) gli stessi principi del compilatore C. Se però si tratta di progetti diversi… facciamo un esempio. Ho tre progetti: X che referenzia Y che referenzia Z e viene modificata l’implementazione di una classe di Z (ma non l’interfaccia): il compilatore ricompila Z e, a cascata, ricompilerà Y e X (!!!).

Nella pratica, se modificherò un dettaglio implementativo del progetto Infrastructure a cascata verranno ricompilati tutti i numerosi progetti che il Dependency Injection mi ha “obbligato” a definire. La ricompilazione può essere anche molto lunga se utilizziamo tool di postcompilazione (come PostSharp) o tool di analisi statica del codice (come i Code Contract) o tool di produttività personale (come Resharper).

Definire tanti progetti ha i suoi vantaggi (es. separazione delle parti e rimpiazzabilità della dll) ma spesso non ben compresi (se rimpiazzo una dll devo rimpiazzare anche le dipendenti?). Avere tante dll in un progetto con IoC/DI diventa quasi un obbligo ma il costo in termini di produttività quotidiana potrebbe essere alto.

Un “nuovo” approccio: sostituzione statica

Ora ragioniamo su un punto… ma è sempre stato così? Come veniva scritto il kernel di Linux?

Ricordiamo che “ai tempi” avevamo l’header file bluetooth.h e il relativo file bluetooth.c.
Il file bluetooth.h contiene i prototipi delle funzioni e il file bluetooth.c contiene le relative implementazioni. Questo significa che se kernel.c fa uso di bluetooth.h allora le funzioni invocate faranno uso dell’implementazione trovata in bluetooth.c. Ma se invece siamo in un sistema a 64 bit il kernel di Linux potrebbe compilare invece il file `bluetooth_64bit.c.

Questo significa, portandolo nel nostro mondo OO, che gli header file sono le nostre interfacce e i file .c sono le nostre implementazioni. Cosa cambia rispetto ai framework IoC-DI? Due semplici cose:

  1. la tecnica esisteva dal 1972 (!)
  2. avveniva tutto a tempo di compilazione

Il punto focale è l’ultimo: avveniva tutto a tempo di compilazione. Pensiamoci bene: rimpiazzare IApplication con ApplicationMock può essere utile per i test, ma non c’è bisogno che sia dinamico, può essere risolto a tempo di compilazione.

Come fare a ottenere questo? In due modi:

  1. sostituendo dll che mutano la risoluzione dinamica del tipo
  2. sostituendo il codice compilato internamente tramite direttive di compilazione condizionata

La prima soluzione diventerebbe:

// FILE Main.CS in Console.dll
public class Main {
    public static void Main() {
        Application app = new Application();
        app.AddPerson(person);
    }
}

// FILE Application.cs in Application.dll dal progetto Application.Real
public sealed class Application {   // real implementation
    public void AddPerson(Person person) {
        PersonService personService = new PersonService();
        personService.Add(person);
    }
}

// FILE Application.cs in Application.dll dal progetto Application.Mock
public sealed class Application {   // mock implementation
    public bool AddPersonInvoked { get; set; }
    public void AddPerson(Person person) {
        this.AddPersonInvoked = true;
    }
}

Se la cartella che contiene l’applicazione contiene i seguenti file:

  • Console.dll
  • Application.Mock.Dll

Allora il tipo Application risolto sarà quello con implementazione Mock,

La seconda soluzione tramite direttive di compilazione condizionata diventerebbe invece:

// FILE Bootstrap_Prod.cs (compilato solo in compilation mode RELEASE)
#DEFINE REAL
#DEFINE REPOSITORY_IN_DATABASE
#DEFINE SECURITY_IN_LDAP

// FILE Bootstrap_Test.cs (compilato solo in compilation mode TEST)
#DEFINE MOCK
#DEFINE REPOSITORY_IN_MEMORY
#DEFINE SECURITY_IN_MEMORY

// FILE Main.CS
public class Main {
    public static void Main() {
        Application app = new Application();
        app.AddPerson(person);
    }
}

// FILE Real\Application.cs
#IF REAL
public sealed class Application {   // real implementation
    public void AddPerson(Person person) {
        PersonService personService = new PersonService();
        personService.Add(person);
    }
}
#ENDIF

// FILE Mock\Application.cs
#IF MOCK
public sealed class Application {   // mock implementation
    public bool AddPersonInvoked { get; set; }
    public void AddPerson(Person person) {
        this.AddPersonInvoked = true;
    }
}
#ENDIF

Notato qualcosa? Non ci sono più interfacce!! Se il progetto viene compilato in modalità RELEASE allora il file Bootstrap_Prod.cs viene incluso e il file compilato sarà Real\Application.cs. La classe Main quando istanzia Application otterrà la versione reale senza alcun bisogno di DI. Il giro funziona bene finquando le due classi REAL e MOCK hanno lo stesso fullname (devono quindi dichiarare lo stesso namespace).

Confrontando le due soluzioni la separazione fisica delle dll è la soluzione più semplice, ma meno flessibile rispetto a quella basata su compilazione condizionata, dove è possibile definire molti attributi che consentono di configurare a tempo di compilazione in modo molto puntuale l’implementazione da fornire.

Vantaggi dell’approccio a sostituzione statica

L’approccio abilita la stessa sostituibilità di implementazione del DI con i seguenti vantaggi:

  • il codice è facilmente navigabile (CTRL+Click)
  • la leggibilità del codice è diretta
  • la costruzione delle istanze è diretta
  • il ciclo di vita delle istanze è semplice da seguire
  • l’analisi statica del codice produce risultati veritieri, diventando uno strumento utile alla qualità del codice
  • i progetti di architettura possono validare il codice
  • errori grossolani come riferimenti circolari sono ora impediti
  • il codice è più performante poiché non ha l’indirezione del container IoC

Purtroppo l’approccio ha anche i suoi svantaggi

Problema 1: disallineamento (solo con la compilazione condizionata)

Il punto forte e il problema al tempo stesso è che viene compilato solo il codice interessato: questo potrebbe portare a non compilare per tanto tempo la versione MOCK con forti rischi di disallineamento. Inoltre strumenti di refactoring andrebbero a propagare i cambiamenti solo nelle parti attualmente compilate.

In questo caso è necessario avere un meccanismo che compili automaticamente nelle varie configurazioni. È sufficiente scrivere un semplice script di build tramite MSBuild o NAnt che genera i diversi binari nelle varie configurazioni e includerlo in un processo automatico di verifica.

Si noti che questo problema si presenta solo con la seconda soluzione (tramite direttive di compilazione condizionata). Nella prima soluzione semplicemente si andrà a creare un pacchetto che avrà o meno una certa dll, ma potranno essere sempre tutte compilate.

Problema 2: rimpiazzare con mock

L’approccio funziona bene con gli stub (implementazioni complete, come repository in memoria) ma funziona male con i mock (implementazioni specifiche del test per forzare un certo comportamento della dipendenza).

I framework di mocking, come Moq, funzionano generando dinamicamente implementazioni “al volo” di interfacce. Spesso questi framework chiedono di poter lavorare su un’interfaccia pura e solo i framework più evoluti possono lavorare con una classe concreta i cui metodi siano ridefinibili da una sottoclasse (virtual). Questo porcherebbe a sporcare tutte le classi con vari virtual in tutti i metodi.

Inoltre non basta riuscire a definire il mock, bisogna anche passarlo nel metodo che si sta testando. Senza IoC la dipendenza non è esternalizzata e non la possiamo impostare, poiché viene costruita all’interno del metodo stesso. In questo caso l’unico modo è fare, come nell’esempio precedente, una vera e propria implementazione mock. Per diminuire il lavoro ripetitivo si può generare il codice tramite T4 Text Templates o creando shim tramite framework molto particolari come Microsoft Fakes.

Problema 3: riuso delle istanze

A volte riusare la stessa istanza all’interno del codice è utile ed esiste il pattern Singleton per questo. Il problema è che il Singleton presenta forti problemi legati al testing e alla concorrenza e rappresenta una scelta architetturale molto vincolante che rende invasivo l’eventuale cambio di rotta.

Per questo si dovrebbe usare un container che abiliti il riuso della stessa istanza. Ragionando in questo modo rischiamo però di avvicinarci progressivamente a una versione “lite” del pattern IoC da cui cerchiamo di allontanarci.

LA SOLUZIONE: Dependency Injection senza interfacce

Bilanciando pro e contro il problema non è tanto la presenza di un framework DI, che ha numerosi vantaggi, quanto il fatto di puntare sempre, ovunque, verso interfacce e astrazioni invece di classi concrete.

La soluzione è scegliere un approccio misto: IoC+DI che non lega verso interfacce ma verso classi concrete costruite o (prima soluzione) in dll differenti o (seconda soluzione) tramite compilazione condizionata*.

Il codice diventa quindi qualcosa del genere:

// FILE Main.CS
public class Main {
    public static void Main() {
        IoC myContainer = GetConfiguredIoC();
        Application app = myContainer.Get<Application>();
        app.AddPerson(person);
    }
}

// FILE Real\Application.cs
#IF REAL
public class Application {  // real implementation
    private PersonService _personService;

    public Application(PersonService personService) {
        _personService = personService;
    }

    public virtual void AddPerson(Person person) {
        _personService.Add(person);
    }
}
#ENDIF

In questo modo si ottengono i vantaggi da entrambi i mondi, possono essere definiti mock con Moq ed essere facilmente testati. L’unico svantaggio che rimane è il virtual necessario a ogni metodo, ma questo è facilmente eliminabile in RELEASE aggiungendo uno step di build personalizzato.

Conclusioni

Le nuove tecniche emergenti TDD, DDD, IoC e DI devono essere valutate in modo critico solo dopo averle conosciute bene. Dopo averle conosciute e averci lavorato mi sento di consigliare l’approccio IoC e DI per i vantaggi sulla testabilità e sul controllo del ciclo di vita delle variabili. Ma mi sento di sconsigliare l’indirezione tramite interfacce che crea problemi sulla leggibilità e sull’analisi statica del codice.

Per questo motivo il compromesso DI+IoC+ClassiConcrete ottenuto tramite separazione dll o compilazione condizionata sembra bilanciare bene i vantaggi di ogni tecnica, facendo tornare “in auge” tecniche che risalgono a 40 anni fa.

Posted in design | No Comments »

Riuso o Automazione – come ottenere un riuso al 100% personalizzabile

Posted by Ricibald on 26th June 2013

Riuso con libertà assoluta di customizzazione… Connubio possibile?

Ricordiamo la metafora per i design pattern:

a * b + a * d = a * (b + d)

Significa che l’essenza dei design pattern e della buona programmazione sta nel riuso: più si riesce a centralizzare maggiore è la consistenza e l’efficienza che si ottiene.
L’efficienza è ovvia: centralizzando riusciamo a non riscrivere codice e quindi a ottimizzare il nostro lavoro. Centralizzare significa ottenere consistenza: se check di errori o log esiste nel metodo centralizzato, sarà propagata ovunque tra i chiamanti.

Esistono però ambiti dove il riuso è necessario ma molto arduo a causa delle esigenze specifiche del contesto. Un esempio tipico sono le UI (User Interface).

Immaginiamo un modello complesso da visualizzare, con 100 proprietà di tipi diversi. Nella visualizzazione o edit del modello dobbiamo visualizzare o modificare in modo consistente tutte le proprietà per conferire un look & feel autorevole. D’altra parte la visualizzazione non può nemmeno essere troppo rigida poiché si perderebbe quella flessibilità per visualizzare o modificare in modo speciale particolari proprietà. Il discorso potrebbe non essere limitato alle singole proprietà ma anche al layout stesso: potrebbe richiedere raggruppamenti speciali opzionalmente collassabili, funzionalità di drag & drop, autocompletamento, dialog ajax, popup di conferma, validazione ajax, …

Come ottenere il riuso con la giusta flessibilità?

Ci sono diverse possibilità in questo caso per ottenere il riuso con la flessibilità desiderata:

  • copia e incolla: più che di riuso si parla di “riciclo”. È una soluzione svantaggiosa? Da moltissimi punti di vista sì, ma la replicazione del codice ha anche vantaggi come la diminuzione del rischio globale (se fallisce una libreria base non fallisce tutto) e libertà assoluta di customizzazione. Ovviamente avremo bassissima consistenza ed efficienza
  • micro funzionalità fornite tramite helper: che siano classi di check, design pattern complessi, risorse (css/js), user control, html helper, aspetti, interceptor o extension method tali componenti di appoggio consentono di scrivere codice di più alto livello disinteressandoci di dettagli tecnici come log, caching, verifica errori, generazione html, transazionalità, undo metodi, … Passando opportuni parametri o implementazioni specifiche (Pattern Strategy) possiamo personalizzarne il comportamento (es. se loggare o meno) rendendole scelte comunque flessibili. Normalmente agiscono però a un basso livello: l’orchestrazione dobbiamo sempre implementarla
  • intera funzionalità autogenerata: ci troviamo nel caso più estremo, rendendo il tutto molto più consistente ma anche molto più rigido. Utilizzando metadati (da file, attributi, reflection, convenzioni, codice tramite model specializzati…) o generando codice che abilita il pattern Template Method (es. partial method) possiamo avere dei gradi di customizzazione sulle singole micro funzionalità e sull’orchestrazione stessa. Questa tecnica funziona bene e ha differenti gradi di libertà ma richiede lo sforzo di costruire un framework la cui definizione di metadati potrebbe essere non banale per ottenere il comportamento “ipotizzato” (il risultato finale potrebbe essere difficile da prevedere e non immediato). Infine, la generazione ha comunque un costo computazionale a ogni pagina generata da valutare.
  • intera funzionalità pregenerata: sembra lo stesso caso precedente, in realtà è una generazione su file che sfrutta internamente gli helper. È come se avessimo scritto noi il codice “a mano” ma in realtà è stato generato su file basandosi su metadati. Lo scopo è quello di ricondursi al secondo caso nella lista, in cui costruiamo ogni micro funzionalità tramite helper, ma risparmiando l’onere di riscrivere l’intera orchestrazione. Questo consente che l’orchestrazione sia liberamente editabile: non si otterrà la consistenza nell’orchestrazione (le modifiche in un flusso condiviso devono essere propagate a mano) ma si guadagna in efficienza. Inoltre non inficia in tempi di calcolo. Le tecniche possono essere applicazioni standalone o integrate nell’IDE come i Text Template (T4) che consentono di definire MVC Template.
  • intera funzionalità pregenerata solo su richiesta, altrimenti autogenerata: è un ibrido tra le due. Di base viene autogenerata la funzionalità magari ottimizzandone i tempi tramite un precaching e solo in caso di esigenze particolari di customizzazione si richiede esplicitamente di creare la funzionalità pregenerata su file su cui si avrà completa libertà di azione, ma partendo da uno scheletro già completamente funzionante. Un ottimo esempio in questo senso è ASP.NET Dynamic Data

Il concetto in questo post è stato generalizzato (guarda caso…) ma si può leggere “funzionalità” come “proprietà da visualizzare in input (html)” e “orchestrazione” come “layout della pagina web”: i principi si applicano quindi tranquillamente anche in un contesto web.

Non esiste solo il riuso classico…

Questo post voleva evidenziare i diversi livelli per generalizzare e abilitare il riuso: spesso ci si basa solo sulla seconda soluzione vedendo il riuso solo come la costruzione di componenti e l’utilizzo sapiente di design pattern. In realtà esiste un livello spesso non esplorato: quello della generazione automatica del codice, che elimina gran parte del lavoro pur conservando una libertà assoluta di customizzazione (come nell’ultimo caso).

Arrivare alla generazione automatica del codice ha un costo e non è detto che paghi sempre. Questi i contesti dove secondo me la generazione automatica paga:

  • UI che gestiscono varie entità in modo per lo più omogeneo
  • workflow che gestiscono varie entità in modo per lo più omogeneo
  • mapping tra entità (invece di utilizzare automapper)
  • codice collante strutturale (flusso application-domain-repository per una certa entity)
  • unit test che seguono un certo standard e certi input limite noti
  • entità e metodi derivati da sorgenti esterne (database, wsdl, xsd, api REST)
  • documentazione dei metodi per soddisfare determinati standard
  • refactoring complessi automatizzabili

Altri casi sono facilmente individuabili: ogni qualvolta ci troviamo a lavorare “come macchine” dovremmo far fare il lavoro “alle macchine” per lasciare a noi i lavori più interessanti.

Buon riuso! Anzi, buon riuso+automazione…!

Posted in pattern | No Comments »

Pattern Flyweight in Pratica: Predisporre un’Applicazione per il Precache

Posted by Ricibald on 18th January 2010

Il pattern Flyweight consente di riutilizzare gli oggetti in modo da poter evitare allocazioni. Questo risulta utile se:

  1. gli oggetti da creare richiedono molta memoria
  2. gli oggetti da creare sono molti
  3. gli oggetti da creare sono costosi da creare (e distruggere) dal punto di vista computazionale
  4. i vincoli impongono un sistema che non abbia cali improvvisi di performance, derivanti ad esempio dall’inizializzazione pesante di un oggetto
  5. i vincoli impongono un utilizzo molto attento delle risorse
Facciamo un esempio pratico di ognuno di questi punti:
  1. un’animazione può avere tante caratteristiche ingenti da memorizzare come accelerazione, direzione, …
  2. un word processor utilizza migliaia di caratteri con le stesse info su font, dimensione, colore, …
  3. un’immagine: il caricamento richiede ogni volta l’accesso al filesystem
  4. un videogioco deve avere fps costanti e non avere improvvisi picchi negativi di 10fps solo perché si alloca una risorsa
  5. un dispositivo mobile come il Nokia o l’iPhone
La parte critica a mio parere non è implementare il pattern direttamente: con un po’ di sforzo si riesce! Il problema sta nell’adattare un progetto non pensato per questo pattern, in questo caso l’implementazione non diventa affatto banale!
Facciamo un esempio pratico: ho un’applicazione per iPhone che deve gestire delle biglie che rimbalzano di numero crescente. All’inizio le prestazioni risultano buone ma poi iniziano a degradare evidenziando due problemi:
  • la memoria inizia a non essere sufficiente per le info necessarie
  • l’allocazione/deallocazione causa picchi negativi di fps, ora (più) evidenti a causa dello stesso degrado delle performance generali
Per questo vorremmo gestire tutti gli aspetti comuni a ogni singola biglia attraverso il pattern Flyweight. Vorremmo quindi gestire:
  • lo stato intrinseco della biglia nel flyweight
    • posizione, comportamento specifico della biglia
  • lo stato estrinseco della biglia nella nostra app, che lo passa al flyweight
    • immagine, colore, dimensione, comportamenti comuni di animazioni
Facile a dirsi, ma se non abbiamo pensato la app in questo modo ci troveremo tanti problemi. Infatti al momento di creare una biglia prima:
  • veniva creata e associata l’immagine corrispondente
  • veniva aggiunta l’immagine alla scena corrente assegnando uno z-index
  • veniva assegnato un comportamento della biglia: quanto rimbalzare, se esplode, …
Capire effettivamente cosa estrarre da tutto questo diventa una sfida a volte irrisolvibile, anche a causa dei vincoli tecnologici a cui ci sottopongono i framework stessi, che non prevedono il riuso delle strutture.
Per questo, a parte la teoria del pattern, bisognerebbe:

capire come predisporre una applicazione per fare uso di un precaching in modo da abilitare il pattern con poco sforzo

Rivediamo quindi l’obiettivo: non vogliamo applicare alla lettera il pattern Flyweight (difficile da adattare al nostro contesto) ma vogliamo evitare allocazioni inutili. Una strategia sta nel riutilizzare le stesse istanze ogni volta che queste non sono più utili. Infatti se una biglia esce dal gioco la strategia comune è deallocare la biglia stessa e allocarne una nuova quando necessario. Invece decidiamo di riutilizzare le stesse istanze quando escono dal campo visivo: ci sarà l’illusione di distruggere la biglia, ma non sarà così! In questo modo possiamo ottenere due vantaggi:
  • l’allocazione e deallocazione, che causavano picchi negativi di fps sono presenti un’unica volta
  • creiamo le premesse per un precaching di ad es. 40 biglie, utile per impedire completamente il problema di picchi negativi di fps
Tornando al problema precedente, quando costruiamo una app non sapremo se questo servirà o meno, ma nel dubbio la cosa conveniente è senza dubbio:

dividere inizializzazione estrinseca da inizializzazione intrinseca in modo da poter riutilizzare l’inizializzazione quando richiesto

Il trucco è quindi proprio questo: nella creazione degli oggetti dovremmo inizializzare solo quello che non cambia nella biglia come l’immagine il colore, … Dopo aver creato gli oggetti passiamo quindi per una seconda fase di inizializzazione che imposta lo stato intrinseco della biglia stessa come la posizione e il comportamento. Questo lo facciamo a priori!! Un piccolo sforzo, ma in fondo ben gestibile. Se un giorno servirà questo ci verrà di aiuto!
Immaginiamo che sia venuto quel giorno! Basta creare una FlyweightFactory che consente il seguente flusso:

  • allo startup la FlyweightFactory crea ad es. 40 biglie e le mantiene in un proprio pool di precaching
  • quando alla app serve una biglia richiede un’istanza disponibile alla FlyweightFactory
  • la FlyweightFactory restituirà un’istanza dal pool con lo stato intrinseco correttamente inizializzato
  • quando la biglia non viene più referenziata nella nostra app ma esiste solo nella FlyweightFactory deve esistere una strategia per marcare l’istanza come disponibile nel pool (es. un meccanismo di notifica a eventi)

Fare questo a fronte della divisione tra allocazione/inizializzazione è un gioco da ragazzi!

A questo punto rimane solo il problema dell’allocazione dello stato estrinseco, che viene ridondata per ogni biglia. Per questo risulta quindi necessario aggiungere come parametro nell’allocazione (creazione) delle biglie lo stato estrinseco stesso, che viene creato un’unica volta dalla factory. In questo modo risolviamo il problema finale.

Il flusso di inizializzazione diventa quindi:

  • allo startup viene inizializzato lo stato estrinseco
  • la FlyweightFactory alloca un pool di 40 biglie passando lo stato estrinseco
  • le singole istanze memorizzano il riferimento allo stato estrinseco
  • la app richiede un’istanza alla FlyweightFactory
  • la FlyweightFactory inizializza un’istanza disponibile dal pool e la restituisce. Rimangono quindi 39 biglie nel pool
  • la app usa la biglia. Quando non serve più la rimuove dalla app
  • la app lancia un evento di avvenuta rimozione della biglia
  • la FlyweightFactory riceve l’evento e aggiunge la biglia nel pool delle istanze disponibili. Rimangono quindi 40 biglie nel pool
Concludendo abbiamo leggermente modificato il pattern Flyweight per adattarlo a contesti reali: il pattern infatti prevede che le singole operazioni portino con sé lo stato estrinseco su cui lavorano mentre la nostra soluzione prevede un’allocazione che assegni lo stato estrinseco e le singole operazioni che lo usano come se lo avessero creato loro. Questo si sposa bene con il refactoring necessario per applicare il pattern, che altrimenti richiederebbe un ripensamento globale dell’applicazione.

Posted in .net, performance | 1 Comment »

Progettare un’Applicazione Web 2.0: Yahoo! Design Pattern Library

Posted by Ricibald on 18th November 2008

Le applicazioni vengono progettate seguendo degli approcci di progettazione formalizzati attraverso lo use case analysis e il conseguente utilizzo dei design pattern. La progettazione affronta problemi come la suddivisione delle responsabilità tra le varie classi e la loro conseguente comunicazione. In seguito, le applicazioni vengono scritte utilizzando delle API per la realizzazione di UI complesse, come le swing per Java o le WinForm per .NET.

Nelle applicazioni web viene però trascurata la progettazione legata all’usabilità e al social network, “delegando” in qualche modo quest’aspetto alla bontà di chi scriverà l’HTML, come se ogni volta ci scrivessimo da zero una dialog windows o il layout a grid in una windows application…

Yahoo! è stata la prima ad affrontare in modo rigoroso il problema dando una risposta a problemi comuni della progettazione delle pagine web:

  • Prestazioni delle pagine:  34 best practices e un plugin per firefox per ricevere al volo i consigli su come ottimizzare la pagina corrente…
  • Librerie UI: componenti come modal dialog, layout manager, treeview, … progettati per essere crossbrowser. Molto spesso si abbinano queste librerie a progetti più generici come JQuery per creare i propri personali effetti grafici non presenti nelle librerie di Yahoo!
  • Design Pattern Library: risolvono problemi ricorrenti nelle applicazioni web

Proprio di quest’ultimo faremo una carrellata dei pattern citati:

Search

  • Search Pagination, Item Pagination e Carousel
    • Problema: si vuole visualizzare il risultato di una ricerca e non è possibile mostrarli tutti in una pagina.
    • Soluzione:
      • creare un paginator che contiene gli elementi “Prev <- 2 3 4 5 6 7 8 9 10 11 -> Next”, dove il link corrente (7) non è cliccabile (Search Pagination).
      • creare una semplice navigazione “1-5 of 32 First | < Prev | Next > | Last” per ogni elemento, in modo da far mantenere all’utente il focus sulla navigazione avanti/indietro (Item Pagination)
      • creare una navigazione avanti/indietro molto grafica, da utilizzare in un ambito dove si scorre tra risultati che prevedono grafica (Carousel)
    • Librerie: YUI Paginator e YUI Carousel

Navigation

  • Breadcrumbs.
    • Problema: l’utente si trova in una pagina del sito da cui non è agevole accedere alle altre sezioni e potrebbe provenire da un altro sito e non avere quindi la percezione di dove si trova collocato nel sito che stà visitando.
    • Soluzione: presentare una lista orizzontale di link che rappresenta un “albero compatto” della collocazione del link corrente all’interno della struttura di un sito
    • Librerie: .NET SiteMapPath
  • Alphanumeric Filter Links.
    • Problema: l’utente ricerca un elemento di cui conosce solo la lettera iniziale.
    • Soluzione: consentire una ricerca partendo dalla lettera iniziale
  • Module Tabs.
    • Problema: esistono diversi pannelli che non possono essere visualizzati contemporaneamente e che comunque non ci sarebbe sufficiente spazio e di cui è necessario eseguire uno switch senza cambiare pagina web corrente.
    • Soluzione: creare una lista di tab separati da un ‘|’, il cui click causa l’attivazione lato client solo del pannello interessato, disattivato il precedente. Il tab è una rappresentazione della categorizzazione delle cartelline di lavoro tramite l’uso di linguette (tab).
    • Librerie: YUI TabView
  • Navigation Tabs
    • Problema: esistono 3-10 macrocategorie sufficientemente stabili da rappresentare, le quali devono essere utilizzate per indicare la posizione dell’utente all’interno del sito. Il cambio di categoria deve causare un completo caricamento di una nuova pagina.
    • Soluzione: utilizzare tab in alto, eventualmente con sottocategorie al click. Se le categorie sono troppe per lo spazio orizzontale, creare i tab sulla sinistra della pagina

Browsing

  • Page Grids
    • Problema: si devono creare/gestire molteplici pagine, le quali potrebbero essere costruite da gruppi di lavoro differenti
    • Soluzione: creare un template comune a griglia, in cui ogni cella è destinata a contenere il lavoro di un gruppo. Bisogna inoltre creare un css comune che contenga quanto richiesto da tutti i gruppi di lavoro
    • Librerie: YUI Grids. Consente di creare layout a grid tramite div e css cross browser.

Selection

  • Auto Complete
    • Problema: si deve inserire in un campo di testo un valore difficile da ricordare e che quindi si presta a errori di scrittura
    • Soluzione: fornire una textbox con supporto all’autocompletamento
    • Librerie: YUI Autocomplete
  • Calendar Picker
    • Problema: l’utente deve inserire o trovare un’informazione basata su data o su data range
    • Soluzione: fornire una textbox o due textbox che propongono di compilare automaticamente il campo sulla base della selezione effettuata dall’utente all’interno di un calendario grafico
    • Librerie: YUI Calendar

Rich Interation

  • Drag and Drop Modules
    • Problema: l’utente deve personalizzare il layout della propria pagina senza lasciare la pagina corrente
    • Soluzione: fornisci il drag dei moduli. Suggerisci dove poter effettuare il drop incorniciando le aree “droppabili” (pattern Drop Invitation)
    • Librerie: YUI Drag & Drop
  • Cursor Invitation
    • Problema: l’utente deve capire che può interagire con un oggetto della pagina
    • Soluzione: cambia il puntatore del mouse a seconda dell’operazione da eseguire
  • Tool Tip Invitation
    • Problema: l’utente deve capire cosa succederà al click del mouse su un oggetto che non sia un semplice link
    • Soluzione: fornisci un tooltip come “Clicca per modificare”
  • Hover Invitation
    • Problema: l’utente deve capire quale effetto scatenerà al “semplice” click del mouse su un oggetto che scatena un certo cambio di stato
    • Soluzione: fornisci un feedback immediato al passaggio del mouse sull’elemento mostrando un avvertimento su cosa succederà
  • Animate Transition
    • Problema: l’utente deve percepire gradualmente che un oggetto sta cambiando la sua relazione con lo spazio occupato nella pagina
    • Soluzione: fornisci un’animazione che gradualmente faccia cambiare la relazione con lo spazio
    • Librerie: YUI Animation
  • Dim Transition e Brighten Transition
    • Problema: l’utente deve percepire che un elemento della pagina è diventato di secondaria importanza, non disponibile o non editabile. Questo stato deve poi poter essere ripristinato, facendo segnalare la rinnovata attività dell’elemento
    • Soluzione: definisci lo stato dim e lo stato brighten. Il primo si verifica nel momento in cui diventa di secondaria importanza, mentre il secondo ripristina l’elemento
    • Librerie: YUI Animation
  • Collapse Transition e Expand Transition
    • Problema: l’utente deve poter comprimere un certo elemento che ritiene di secondaria importanza. Viceversa, l’utente deve poter espandere un elemento precedentemente compresso o ottenere dettagli su un certo elemento
    • Soluzione: comprimi o espandi utilizzando una rapida animazione (0.5 secondi)
    • Librerie: YUI Animation
  • Cross Fade Transition
    • Problema: l’utente deve percepire che la vista di un oggetto sta per essere rimpiazzata con una nuova vista
    • Soluzione: esegui il fade out della rimpiazzata, mentre la nuova entrerà in fade in
    • Librerie: YUI Animation
  • Fade In Transition e Fade Out Animation
    • Problema: l’utente deve percepire che un elemento è stato aggiunto o rimosso alla pagina
    • Soluzione: porta l’opacità di un oggetto dal 0% (100%) al 100% (0%) eventualmente attivando (disattivando) il focus sull’elemento
    • Librerie: YUI Animation
  • Self Healing Transition
    • Problema: l’utente deve percepire che un elemento all’interno di una lista è stato rimosso
    • Soluzione: esegui il fade out dell’elemento lasciando un buco sull’elemento rimosso. Successivamente esegui il pattern Slide.
  • Slide Transition
    • Problema: l’utente vuole percepire che un elemento non popup è stato aggiunto o rimosso dalla pagina e vuole percepire la collocazione spaziale del nuovo elemento all’interno della pagina
    • Soluzione: unisci il fade con lo slide
    • Librerie: YUI Animation
  • Spotlight
    • Problema: l’utente vuole percepire che un valore è cambiato nell’interfaccia
    • Soluzione: cambia colore dell’elemento istantaneamente in modo che sia visibile e ripristina al colore del background nel giro di un secondo
    • Librerie: YUI Animation

Social: Rating & Reviews

  • Architecture of a Review
    • Problema: una webapp deve presentare voti e recensioni con una varietà di informazioni aggiuntive
    • Soluzione: gli elementi devono essere raggruppati per Target Element (info sul prodotto da recensire), Review Element (voti + recensioni) e Form Element (giudizio utente). Ogni elemento può essere ulteriormente scomposto secondo la struttura riportata nel pattern
  • Rating an Object
    • Problema: un’utente vuole lasciare la propria opinione su un oggetto, con interruzioni minime a qualsiasi altro task su cui stà lavorando
    • Soluzione: mostra votazione pronta all’uso, combinando la tecnica Hover Invitation e invitando l’utente a votare in presenza di un valore vuoto
  • Vote to Promote
    • Problema: l’utente vuole promuovere un contenuto in una comunity in modo che tale contenuto abbia maggior rank e venga mostrato maggiormente, in una forma democratica
    • Soluzione: fornisci un meccanismo di voto “one-time” per l’utente; evidenzia gli elementi più votati mostrandone il numero di voti; fornisci il voto solo a seguito del consumo dell’elemento: alla fine dell’articolo o all’interno delle pagine esterne tramite snippet esterno da includere
  • Writing a Review
    • Problema: l’utente vuole condividere la propria opinione su un oggetto in modo più accurato di un semplice voto
    • Soluzione: fornisci una form di review che contenga info quantitative (voti vari) più info qualitative (recensione stessa) con associate delle linee guida e varie altre regole

Social: Reputation

Un sistema di reputazione viene definito nel momento in cui si ha necessità di garantire un maggiore utilizzo del servizio o una qualità nell’utilizzo. Il pattern The Competitive Spectrum fornisce delle linee guida per scegliere il tipo di sistema di reputazione da realizzare. Nel pattern, la community viene definita in termini di competitività: la combinazione degli obiettivi individuali, delle azioni che influiscono sui bisogni della comunità e del grado richiesto di confronto tra i membri. Le linee guida da adottare dipendono dal livello di competitività richiesta:

  • Caring
    • Obiettivo: i membri vogliono aiutare altri membri
    • Reputazione: consente di far identificare i membri “senior” all’interno della community
    • Strumenti: fai indossare una o più Identifying Labels al profilo di un utente in modo che i “bollini” guadagnati caratterizzino l’influenza del membro nella community
  • Collaborative
    • Obiettivo: i membri vogliono cooperare per raggiungere un obiettivo in gran parte condiviso
    • Reputazione: consente di far identificare i membri che sono fedeli alla community
    • Strumenti: decora il profilo utente con dei Named Levels che consentano di stabilire facilmente, senza ambiguità e senza alcuna accezione offensiva la fedeltà dei membri
  • Cordial
    • Obiettivo: i membri hanno i loro personali scopi, ma questi non vanno a contrastare con quelli condivisi dalla community
    • Reputazione: consente di far identificare i membri che hanno valori e interessi che la community reputa positivi
    • Strumenti: fornisci dati statistici correlati a un utente. In aggiunta, il pattern Top X consente di identificare facilmente gli utenti che producono interventi con maggiore qualità
  • Competitive
    • Obiettivo: i membri hanno gli stessi obiettivi, ma per raggiungerli devono competere tra di loro
    • Reputazione: consente di far mostrare alla community i progressi raggiunti da un certo utente per suscitare ammirazione (o invidia)
    • Strumenti: fornisci una facile comparazione decorando al profilo un “livello saiyan” agli utenti tramite dei Numbered Levels. Fornisci delle soddisfazioni tramite premi come “luccicanti badge da decorare al profilo” tramite l’uso dei Collectible Achievements, in cui i premi sono legati a diversi obiettivi di difficoltà crescente (da “hai completato il profilo” a “sei il più attivo nella community”)
  • Combative
    • Obiettivo: i membri condividono obiettivi opposti. Il raggiungimento dell’obiettivo di uno nega il raggiungimento dell’obiettivo dell’altro.
    • Reputazione: consente di far mostrare alla community i progressi e i regressi raggiunti da un certo utente per suscitare ammirazione o scherno
    • Strumenti: utilizza il pattern Points per rappresentare la performance di un utente e poterla incrementare o diminuire a seconda dei risultati. Fai un rank dei membri tramite o Top X o una Leaderboard (un Top X più aggressivo, che rappresenta ad esempio le top 3 della settimana)

A queste tecniche spesso si combina quella della Sign-in Continuity, in cui il voto di un utente viene proposto a patto di registrarsi o di autenticarsi, ma la registrazione non deve perdere il contesto (utile una dialog).

Posted in .net | 2 Comments »

Metafora Aritmetica per i Design Pattern

Posted by Ricibald on 13th August 2008

Dal blog di Adrian Florea (e quindi dal libro di Uwe Aßmann) mi ha colpito molto questa metafora aritmetica per esprimere il concetto dei design pattern:

The basic solution strategy of a design pattern is factoring:

a * b + a * d = a * (b + d)

Design patterns are the “binomial formulas” of software engineering!

Infatti quello che fanno i design pattern è fattorizzare, mettere in comune aspetti altrimenti ripetuti, come:

  • porzioni di codice in un metodo: Pattern Template
  • espressioni, salti condizionati: Pattern Strategy, State, Template, Factory, … in generale l’utilizzo sensato del polimorfismo (cioè il senso dei pattern)
  • metodi interi: spesso sintomo di cattiva assegnazione di responsabilità. I pattern consentono di individuare le responsabilità in modo da condividere gli stessi metodi nell’ambito della stessa classe padre

Un ottimo articolo che ne parla è quello di UGIdotNET.

Posted in .net | 3 Comments »

Applicazioni Distribuite: Proprietà Statiche vs Caching. Singleton rivisitato

Posted by Ricibald on 13th August 2008

Normalmente quando scriviamo codice capita di scrivere proprietà statiche: si pensi al pattern Singleton. Lo scopo di questo pattern è proprio assicurare la condivisione degli elementi, per ragioni di efficienza o di consistenza.

Il pattern funziona bene in contesti “normali”: è implementato mediante un campo/proprietà statico e quindi condiviso nell’ambito dell’applicazione Console/Windows che sviluppiamo. Se però ci spostiamo in contesti distribuiti il pattern non è in realtà così efficace:

In contesti distribuiti, i campi statici sono condivisi solo nell’ambito della stessa richiesta

Quindi in una nuova richiesta viene “dimenticato” il valore assunto degli stessi campi statici in altre richieste. I campi statici nascono invece con lo scopo di avere memoria globale di un’informazione. Quindi, come regola generale:

Il concetto di “campi static” dovrebbe sempre essere virtualizzato

Questo significa che, come possibile soluzione, la proprietà statica deve essere acceduta mediante una classe astratta CacheManager, che:

  1. è ottenibile come Singleton “puro”, cioè come proprietà statica a cui siamo tanto abituati. Avremo quindi un metodo CacheManager.GetInstance che restituisce tale istanza, condivisa solo nell’ambito della richiesta corrente
  2. consente metodi come SetPrinterSingleton o GetPrinterSingeton che restituiscono tali valori in cache
  3. l’implementazione di tale metodi varia a seconda del tipo concreto in uso

Il terzo punto significa che avremo diverse implementazioni, come StaticCacheManager o WebApplicationCacheManager, in cui la prima prenderà valori nel modo classico, tramite proprietà di istanza, mentre la seconda implementazione li prenderà dalle variabili “Application” del nostro web site.

La scelta di quale implementazione concreta utilizzare è demandata al momento in cui viene creato internamente l’istanza unica di CacheManager, che ci restituirà l’istanza corretta sulla base del principio “Dependency Injection: viene specificato cosa creare mediante file di configurazione e/o Reflection.

Molto comodo risulta infine utilizzare un Singleton con “shortcut”: invece di scrivere

public abstract class CacheManager {
    public static CacheManager GetInstance() {
        return ServiceLocator.GetCacheManager(); // usa reflection
    }
    public abstract Printer GetPrinterSingleton();
}

scriveremo una forma molto più compatta da utilizzare per il “client” della classe:

public abstract class CacheManager {
    public static CacheManager GetInstance() {
        return ServiceLocator.GetCacheManager(); // usa reflection
    }
    public static Printer GetPrinterSingeton() {
        return GetInstance().GetPrinterSingleton();
    }
    protected abstract Printer GetPrinterSingleton();
}

Che verrà quindi utilizzato in questo modo:

public sealed class Printer {
    private Printer() {
    }
    public static Printer GetInstance() {
        // lock regione critica omessi per semplicita'
        Printer printer = CacheManager.GetPrinterSingleton();
        if(printer == null) {
            // il set restituisce la stessa istanza passata per parametro
            printer = CacheManager.SetPrinterSingleton(new Printer());
        }
        return printer;
    }
}

Si può anche esprimere in una forma succinta, utilizzando l’operatore di null coalescing di C#:

public sealed class Printer {
    private Printer() {
    }
    public static Printer GetInstance() {
        return CacheManager.GetPrinterSingleton() ?? CacheManager.SetPrinterSingleton(new Printer());
    }
}

Si può infine completare l’implementazione garantendo la mutua esclusione utilizzando l’approccio di William Pugh:

public sealed class Printer {
    private Printer() {
    }
    public static Printer GetInstance() {
        return SingletonHolder.GetInstance();
    }

    class SingletonHolder {
        static SingletonHolder() { }
        static Printer GetInstance() {
            return CacheManager.GetPrinterSingleton() ?? CacheManager.SetPrinterSingleton(new Printer());
        }
    }
}

Attenzione: si potrebbe pensare che lo stesso concetto si applichi anche al pattern Flyweight. In realtà questo pattern (a differenza dell’esempio riportato nell’articolo in Wikipedia) non dovrebbe prevedere una mappa statica, ma a livello istanza, quindi la regola descritta è comunque soddisfatta.

In pratica la regola da seguire è questa:

Campi/proprietà statiche hanno senso solo nel contesto del pattern Singleton, e in tal caso il concetto di “campi static” deve essere virtualizzato tramite un CacheManager, a sua volta creabile tramite la versione “standard” del pattern Singleton

Posted in .net | 12 Comments »

Approcci in .NET e Windows Server 2003 per Ottenere la Scalabilità delle Applicazioni

Posted by Ricibald on 22nd January 2008

In .NET è spesso necessario ottenere la scalabilità delle applicazioni. Immaginiamo di aver scritto un bellissimo software che richiede a un sistema esterno il permesso per richiedere un mutuo aziendale. Saranno presenti numerosi scambi di messaggi tra il nostro sistema e il sistema esterno in questione, che possono comportare dei lunghi tempi di attesa.

Immaginiamo ora che in questo lasso di tempo avvengano 100.000 richieste verso la nostra applicazione. Possiamo immaginare soluzioni più o meno eleganti, come pool di thread o gerarchie di processi ma queste hanno efficacia solo con molteplici processori. Anche se è una soluzione che solo ora sta diventando praticabile per un computer “normale”, grazie ai processori multi core, non è comunque una soluzione “altamente” scalabile: è infatti subordinata all’architettura del sistema che esegue la nostra applicazione.

Si utilizzano quindi normalmente diversi approcci di computer clustering. Tra i possibili tipi di clustering, esistono:

  • Fail-over clustering: il funzionamento delle macchine è continuamente monitorato, e quando uno dei due host smette di funzionare l’altra macchina subentra. Lo scopo è garantire un servizio continuativo
  • Load-balancing clustering: sistema nel quale le richieste di lavoro sono inviate alla macchina con meno carico
  • High Performance Computing clustering: le macchine suddividono i processi di un job su più macchine, al fine di guadagnare in prestazioni

In realtà, i clustering descritti sono figli di due pattern (presi da POSA):

  • Broker (o la sua versione “leggera” Client-Dispatcher-Server): supporta il coordinamento e la comunicazione tra sistemi distribuiti eterogenei. Consente di mantenere trasparente la localizzazione dei Server, in modo da poterli cambiare dinamicamente per ottenere un bilanciamento di carico
  • Master-Slave: supporta tolleranza d’errore, calcolo parallelo e accuratezza computazionale tramite ridondanza. Esiste un componente Master che suddivide e distribuisce il lavoro a componenti Slave identici. Gli Slave calcolano il risultato parziale e lo restituiscono al Master, il quale assembla i risultati parziali ottenuti per ottenere quello definitivo

Il protocollo con cui questi sistemi dialogano non è un problema, se si combina anche il pattern Forward-Receiver, che consente di definire un protocollo di comunicazione trasparente.

Nel nostro caso, risulta eccessivo dividere il lavoro in più sottolavori: quello che vorremmo è semplicemente smistare la richiesta al server più libero in modo da ottenere una scalabilità. Vorremmo quindi implementare il pattern Broker.

Fortunatamente, .NET ci viene incontro e ci fornisce un pattern Broker “pronto all’uso”: il .NET remoting. In MSDN esiste infatti la guida su come implementare il pattern Broker utilizzando .NET remoting.

Ma se si utilizza Microsoft Windows Server 2003, allora è possibile utilizzare una grande scorciatoia per il Load-Balancing clustering (oltre a quella appena vista): le Windows Server 2003 Clustering Services. Consentono di costruire un cluster di server. Il cluster è dotato di un indirizzo IP virtuale (xxx.yyy.zzz.kkkk), che quando richiesto trova corrispondenza con un indirizzo IP reale della macchina fisica più libera. In questo modo, è sufficiente creare in alternativa:

  • una dll acceduta tramite .NET remoting
  • un web service

e replicarlo in tutte le macchine fisiche. A questo punto, quando accederemo all’indirizzo IP xxx.yyy.zzz.kkkk e richiederemo il servizio interessato, il sistema operativo ci smisterà automaticamente alla macchina più libera. Non c’è bisogno di alcuna modifica nel nostro codice, stupefacente no??

Per maggiori informazioni, si veda l’ottimo articolo su come scrivere applicazioni “clusterizzate” in c# di Gerald Gibson Jr.

Posted in .net | No Comments »

Virtual proxy: una trappola senza uscita?

Posted by Ricibald on 20th July 2007

I virtual proxy forniscono un rappresentante di un oggetto in modo da gestirne il caricamento su richiesta, anche noto come lazy initialization.

Se immaginiamo un file word di 500 pagine, con più di 1000 immagini, il caricamento dovrebbe impegnare molto tempo e molte risorse. In realtà quel che succede è che ogni immagine non viene caricata, ma solo “dichiarata”. Nel nostro file “PROVA.DOC” ci saranno informazioni utili come width/height dell’immagine in modo che la “dichiarazione” dell’immagine consenta la corretta impaginazione.

Fin qui tutto bene, ma ora immaginiamo di aver fatto scorrere tutte le 500 pagine e di aver visto tutte le 1000 immagini. Cosa succede? Semplice: tutte le immagini sono state effettivamente caricate e abbiamo consumato molta, molta RAM (aaarghhh!!) .

Il problema (quì molto romanzato…) sembra però più serio del previsto. Citiamo il pattern Proxy:

Il design pattern Proxy fornisce un surrogato o un segnaposto di un altro oggetto per controllare l’accesso a tale oggetto.

Il client che utilizza il Proxy ne è inconsapevole: sa solamente che il comportamento atteso della classe deve essere rispettato. L’indirezione introdotta dal Proxy deve quindi essere trasparente e l’utente non può intervenire su di essa. Il clean quindi non può (e non deve) essere gestito manualmente.

Abbiamo quindi il seguente schema:

  • una classe R reale
  • un virtual proxy V che gestisce il caricamento su richiesta di R
  • un client, che utilizza V ma che in realtà è convinto di utilizzare R

Dopo tanto patire, senza ulteriori preliminari, questa è la soluzione che mi sembra più corretta (vi apparirà forse scontata ora che la leggete, ma vi garantisco che cercando e ricercando su Google nessuno ne parla…). Il nuovo contesto è il seguente:

  • una classe R reale
  • un cache proxy C, che memorizza fino a n classi reali R
  • un virtual proxy V, che se necessario richiede a C di ottenere la classe reale R
    • C restituisce la cache entry se esiste, o crea il corrispondente R memorizzando il risultato in cache
    • se nella memorizzazione del nuovo cache entry viene superato il limite n di cache:
      • viene liberata la cache eliminando l’elemento meno richiesto (least frequently used)
      • vengono notificati tutti gli V che ne facevano uso, che impostano a null la classe reale R utilizzata richiamando funzioni di liberazione della memoria come la garbage collection. La notifica si deve basare su un protocollo condiviso tra V e C, utilizzando ad esempio il design pattern Observer.
  • un client, che utilizza V ma che in realtà è convinto di utilizzare R

Un modo semplice di implementare la tecnica “least frequently used” è la strategia “move-to-front” descritta in Pattern Oriented Software Architecture: a ogni richiesta di una cache entry l’elemento viene spostato alla testa di una lista. Quando è necessario liberare memoria possono essere eliminati gli elementi alla coda della lista. Questo può essere implementato (ad es. in Java) estendendo i metodi get() e put() di una Map.

Questa (da quanto ne so) è una soluzione “casalinga”, che non si basa su tecniche o pattern noti. Perciò qualunque suggerimento/critica sarà più che apprezzata…

Posted in pattern | 1 Comment »