Wrappa che ti passa: “how to in salsa TDD”…?
Facciamo un po’ di storia. C’erano una volta i cursori e i lock pessimistici:
poi, forse fortunatamente, il mondo si è evoluto e siamo passati agli scenari disconnessi:
In uno scenario disconnesso semplicemente si carica/crea una rappresentazione in memoria dei dati e si lavora su quella rappresentazione invece che direttamente sullo storage.
Questo secondo approccio risolve molti problemi, permette scenari di gran lunga più mirabolanti ma è evidente che porta con se qualche magagna come ad esempio la gestione della concorrenza ottimistica che ha una notevole quantità di vantaggi ma non è certo semplice soprattutto nel suo rapporto con l’utente.
Inoltre in un rich-client degno del suo nome ci portiamo a casa una serie di magagne non indifferenti principalmente legate alla user experience, vediamone un paio:
…e lui ci chiese di crearne uno nuovo…
Tenendo bene a mente il disegnino di cui sopra provate a calarci questa entità di dominio:
“Come utente voglio poter creare un nuovo User scegliendo il nome utente che, se valido, non potrà più essere modificato”Una entità di dominio del genere è decisamente allergica alla UI e vi obbliga a modellare la UI secondo le sue necessità, ad esempio con uno wizard, e non secondo le necessità del vostro utente. Quello che voglio dire è che è decisamente “difficile” mettere quella entità, altrettanto decisamente lecita, in binding con un editor perchè l’utente deve poter editare la proprietà Username ma il suo valore vi serve prima dell’inizio dell’editing al fine di poter creare un’istanza dell’entità da editare… c’è qualcosa che non va ;-) e diciamo subito che non è l’entità di dominio il problema ma probabilmente è il fatto che vi manca qualche cosa nel mezzo. Ma non solo…
…e lui scelse qualcosa da modificare…
Il nostro “simpatico” utente cerca qualcosa, visualizza una lista e sceglie un elemento da visualizzare, apre uno screen di edit e comincia l’editing… dato che la nostra bella entità implementa INotifiyPropertyChanged ed è shared tra la lista e lo screen di editing abbiamo l’effetto collaterale che le modifiche sono immediatamente visibili anche nella lista.
Dal punto di vista del codice non è un problema particolare perchè ci basta implementare un meccanismo come quello offerto da IEditableObject (che a me non sconquinfera più di tanto… ma è un’altra storia) per garantire la consistenza dei dati, l’inghippo è che l’effetto che otteniamo non è consistente per l’utente che, giustamente, non ha le nozioni (e non deve averle) per capire cosa sta succedendo e perchè.
Presentation Pattern
entrambi gli esempi di cui sopra sono uno dei motivi per cui esistono i “presentation pattern” e sono uno dei motivi per cui i presentation patter sono un trittico. In tutti i casi c’è sempre un attore tra il model e la sua presentazione:
- Model View Controller;
- Model View Presenter;
- Model View ViewModel;
è un mondo dificile
In questo scenario è il mediatore che contiene i dati e fa il commit verso la nostra bella entity solo ed esclusivamente all’atto del commit da parte dell’utente. In questo caso la nostra entità non implementa nulla di relativo al presentation, come è giusto che sia, quindi ad esempio non implementa INotifyPropertyChaged, è il “mediatore” che ha tutta la logica per interagire con la UI, è il mediatore che fa la validazione contestuale a quella specifica fase di editing è il mediatore che se necessario implementa la logica per la gestione del memento… more to come ;-)
E’ però evidente, come abbiamo constatato con Raffaele, che wrappare costa un sacco perchè se immaginate di espandere il vostro dominio aggiungendo una banale relazione uno-molti è evidente che le cose si complicano non poco perchè dovete wrappare l’impossibile con un bel po’ di codice da scrivere.
Please welcome ICustomTypeDescriptor
Ma non tutto è perso, altrimenti che scriverei a fare :-), possiamo infatti con un po’ di lavoro simpaticamente ingannare il motore di binding di Wpf (anche tutti gli altri), osservate questo esempio basato sulla classe User di cui sopra:
Che produce questo risultato:<Window.Resources> <local:SampleDataContext x:Key="sdc" /> Window.Resources> <StackPanel DataContext="{StaticResource sdc}"> <StackPanel> <Border Margin="2" BorderThickness="1" BorderBrush="Red"> <TextBox Text="{Binding Path=DataSource.FirstName, UpdateSourceTrigger=PropertyChanged}" /> Border> StackPanel> <StackPanel> <TextBlock Text="Questo testo è in binding con User.FirstName" /> <Border Margin="2" BorderThickness="1" BorderBrush="Red"> <TextBlock Text="{Binding Path=DataSource.FirstName}" /> Border> StackPanel> StackPanel>
Ma… bada ben… il DataContext è la classe SampleDataContext (il mediatore, il ViewModel in M-V-VM) che è così definita:
Togo :-) e anche abbastanza semplice (circa 1h di lavoro, di cui la stragrande maggioranza dedicata alla ricerca, riuscita, di performance, alla fine ci ho messo di più per scrivere questo post). Il segreto, di pulcinella, è proprio l’interfaccia System.ComponentModel.ICustomTypeDescriptor:class SampleDataContext { public SampleDataContext() { this.DataSource = new Wrapper<User>(); } public Wrapper<User> DataSource { get; private set; } }
- Il motore di binding analizza il tipo in binding, in questo caso DataContex.DataSource;
- se si accorge che implementa ICustomTypeDescriptor e delega all’istanza di ICustomTypeDescriptor l’onere di risolvere le proprietà con cui andare in binding;
- Fa il resto della parte di binding come sempre;
ecco che “inganniamo” il motore di binding, per ora però semplicemente lo inganniamo perchè appena proviamo a scrivere qualcosa nella textbox giustamente si schianta tutto perchè non c’è un’istanza su cui fare il set vero e proprio di quel valore.public class SampleWrapper: ICustomTypeDescriptor { readonly PropertyDescriptorCollection wrappedProperties; public SampleWrapper() { this.wrappedProperties = TypeDescriptor.GetProperties( typeof( T ) ); } //... omissis... object ICustomTypeDescriptor.GetPropertyOwner( PropertyDescriptor pd ) { return this; } PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties( Attribute[] attributes ) { return this.wrappedProperties; } }
Vi ricordate le mie Dependency Property? ecco… ripartendo da li, cosa che avevo parzialmente abbandonato perchè non vedevo un’utilità realmente concreta, adesso ho questi test che passano che è un piacere:
Notate che non abbiamo creto un’istanza della classe User? eppure i test passono e la UI in Wpf (utilizzando un minimale M-V-VM) funziona senza scrivere pressochè una virgola di codice, abbiamo infatti una implementazione di INotifyPropertyChanged e abbiamo pure una implementazione del Memento Pattern, implementazione di cui parleremo prossimamente, che ci consente un rollback delle modifiche (bidirezionale: forward changes e backward changes) effettuate dall’utente.[TestMethod] public void wrapper_getPropertyValue_wrapping_nothing_should_return_expected_default_value() { var target = new Wrapper<User>(); var actual = target.GetPropertyValue( u => u.Username ); actual.ShouldBeNull(); } [TestMethod] public void wrapper_setPropertyValue_wrapping_nothing_should_set_value_as_expected() { var expected = "Mauro"; var target = new Wrapper<User>(); target.SetPropertyValue( u => u.FirstName, expected ); var actual = target.GetPropertyValue( u => u.FirstName ); actual.ShouldBeEqualTo(expected); } [TestMethod] [ExpectedException( typeof( InvalidOperationException ) )] public void wrapper_setPropertyValue_using_readonly_property_should_raise_InvalidOperationException() { var target = new Wrapper<User>(); target.SetPropertyValue( u => u.Username, "sample" ); }
Ritorniamo però ia nostri 2 esempi di prima, nel primo caso l’utente desidera editare un nuovo “User” e giustamente deve poter fare il “set” della proprietà Username nonostante questa sia reaonly:
possiamo quindi iniettare nel nostro wrapper un comportamento diverso dal default, questo risolve una parte del problema ma non tutto, infatti abbiamo bisogno di poter eseguire il “commit” delle modifiche nella entità vera e propria:[TestMethod] public void wrapper_setPropertyValue_using_readonly_property_overriding_metadata_should_write_expected_value() { var expected = "sample"; var metadata = new WrappedPropertyMetadata() { IsReadOnly = false }; var target = new Wrapper<User>(); target.OverridePropertyMetadata( u => u.Username, metadata ); target.SetPropertyValue( u => u.Username, expected ); var actual = target.GetPropertyValue( u => u.Username ); actual.ShouldBeEqualTo( expected ); }
abbiamo quindi la necessità di intervenire nel processo di commit, momento in cui il Wrapper travasa i dati nella entità, e dato che stiamo creando un nuovo User iniettiamo nel Wrapper un interceptor (licenza poetica da Castle Windsor) che utilizziamo per creare l’istanza di un nuovo User passando il valore della proprietà username nel costruttore.[TestMethod] public void wrapper_commit_wrapping_nothing_should_create_new_entity_instance() { var expected = "[email protected]"; var propertyMetadata = new WrappedPropertyMetadata() { IsReadOnly = false }; var wrapperMetadata = new WrapperMetadata<User>() { CommitInterceptor = e => { var username = e.Wrapper.GetPropertyValue( u => u.Username ); e.Instance = new User( username ); } }; var target = new Wrapper<User>( wrapperMetadata ); target.OverridePropertyMetadata( u => u.Username, propertyMetadata ); target.SetPropertyValue( u => u.Username, expected ); User actual = target.Commit(); actual.Username.ShouldBeEqualTo( expected ); }
Ok, ma il resto delle proprietà? Abbiamo 2 strade: farcelo noi ;-)
oppure aspettarci che il Wrapper sia in grado di farlo da solo…[TestMethod] public void wrapper_commit_wrapping_nothing_should_create_new_entity_instance_and_should_set_all_values_using_interceptor() { var expected = "[email protected]"; var firstName = "Mauro"; var lastName = "Servienti"; var propertyMetadata = new WrappedPropertyMetadata() { IsReadOnly = false }; var wrapperMetadata = new WrapperMetadata<User>() { CommitInterceptor = e => { e.Instance = new User( e.Wrapper.GetPropertyValue( u => u.Username ) ); e.Instance.FirstName = e.Wrapper.GetPropertyValue( u => u.FirstName ); e.Instance.LastName = e.Wrapper.GetPropertyValue( u => u.LastName ); e.Handled = true; } }; var target = new Wrapper<User>( wrapperMetadata ); target.OverridePropertyMetadata( u => u.Username, propertyMetadata ); target.SetPropertyValue( u => u.Username, expected ); target.SetPropertyValue( u => u.FirstName, firstName ); target.SetPropertyValue( u => u.LastName, lastName ); User actual = target.Commit(); actual.Username.ShouldBeEqualTo( expected ); actual.FirstName.ShouldBeEqualTo( firstName ); actual.LastName.ShouldBeEqualTo( lastName ); }
Notate che nell’interceptor non facciamo più il set di FirstName e LastName? è infatti il Wrapper che adesso fa il commit delle proprietà per noi.[TestMethod] public void wrapper_commit_wrapping_nothing_should_create_new_entity_instance_and_should_automatically_set_all_values() { var expected = "[email protected]"; var firstName = "Mauro"; var lastName = "Servienti"; var propertyMetadata = new WrappedPropertyMetadata() { IsReadOnly = false }; var wrapperMetadata = new WrapperMetadata<User>() { CommitInterceptor = e => { e.Instance = new User( e.Wrapper.GetPropertyValue( u => u.Username ) ); } }; var target = new Wrapper<User>( wrapperMetadata ); target.OverridePropertyMetadata( u => u.Username, propertyMetadata ); target.SetPropertyValue( u => u.Username, expected ); target.SetPropertyValue( u => u.FirstName, firstName ); target.SetPropertyValue( u => u.LastName, lastName ); User actual = target.Commit(); actual.Username.ShouldBeEqualTo( expected ); actual.FirstName.ShouldBeEqualTo( firstName ); actual.LastName.ShouldBeEqualTo( lastName ); }
La domanda più che lecita è: perchè tutto questo sbattimento?
Quello che segue è un parte del dominio di una parte dell’applicazione a cui sto lavorando… una parte di una parte:
Pensate sia realistico pensare di scrivere wrapper veri e propri per tutta quella roba? considerando che:
- Per ognuna delle entità avete mediamente 3 scenari di edit/creazione diversi in base al contesto d’uso;
- Per ognuna delle entità dove implementare il supporto per il “memento” dell’intero grafo che state editando;
Cosa ci manca? ancora 2 cose, di cui una al momento già funzionate:
- Wrapper di una entità esistente: questo funziona, tralascio i test perchè vi ho già tediato abbastanza;
- Scenario con grafo complesso: questo è un filino più complesso ma ho già in mente una soluzione e se funziona dovrei cavarmela con un centinaio di righe di codice. Ho però la possibilità di calare quello che ho realizzato fino ad ora in uno scenario reale per capire se riduce effettivamente i tempi di sviluppo, cosa che sulla carta è già una realtà;
…e anche stavolta ho scritto un poema, scusate :-)
.m
n.d.r.
Probabilmente non lo si è notato ma il tutto è stato realizzato in TDD, non che sia importante ma mi piaceva sottolinearlo, in questa caso TDD mi è stato utilie perchè mi ha aiutato a focalizzare l’attenzione sulle feature che mi servivano senza lasciarmi trascinare dall’euforia del programmatore moderno :-P
Ad esempio mi è venuto in mente che sarebbe carino avere il supporto per delle custom property che non esistono sul modello e per una sorta di interceptor per il get/set di ogni singola property, ma per ora non servono, per ora :-)