Finalmente trovo il tempo e l'occasione per iniziare a
raccontare alcune delle soluzioni che ho utilizzato nei miei progetti
reali. Mi piacerebbe pensare ad una serie di post con questo "tema
portante", ma per ora mi limito a trovare le energie per focalizzarmi su un
solo argomento specifico: la validazione delle regole di "univocità".
Questo è il racconto delle ultime giornate di lavoro, caratterizzate da
un continuo refactoring: tanta "irruenza" e "coraggio" nel
modificare quanto appena raggiunto, se in mente c'è un'idea nuova e
affascinante. Tutto questo reso possibile soltanto grazie ad
una serie di test case che in ogni momento mi raccontano dove ho
introdotto anomalie o mi confermano che tutto prosegue bene!
Ecco lo scenario: operazione di dominio per l'assegnamento ad un
account di parametri univoci, come nome o mobile. La struttura che ho realizzato
è composta così:
- layer dominio (core)
- layer business (application)
- layer presentation (ui)
Non introduco troppo dettaglio, visto che in questo post non
voglio parlare della mia architettura (argomento solo rimandato!) ma di una
soluzione di object design.
Ho introdotto due livelli di validazione:
- a livello di oggetto del dominio (regole logiche generiche, poco
stringenti, come not null, stringhe senza spazi e
l'ambito di univocità), valide cioè per il dominio applicativo in questione
- a livello di controllori dell'applicazione (regole dettagliate, come
espressioni regolari sulle stringhe, etc.), relative quindi ad una singola
applicazione (come un intero sito web)
La suddivisione che ho scelto è stata dettata dalla
necessità di riusare gli oggetti del dominio in più contesti, mentre la
realizzazione di un caso d'uso ha le sue regole specifiche che ho accentrato nei
controllori dello strato business.
CHE NOIAAA!! Sì sì
lo so, sembra di leggere un manuale di ingegneria del software!
Pare tutto così ovvio! Certo, ma mi interessava
spiegare "il perchè" ho messo regole di validazione nel dominio,
altrimenti più che stimolarvi a discutere con me della soluzione di cui ora
voglio parlare, probabilmente avrei fatto nascere solo dubbi sul fatto che
queste regole fossero proprio lì! No, per ora non mi occupo di
questo!
[tante parole e ancora non ho detto nulla!]
Allora, eccomi al sodo!
Voglio descrivere una soluzione che ho
utilizzato per questo problema: partire da una classe base astratta per gli account,
ed estenderla per strutturare le figure specifiche del dominio, come i vari
gradi di amministratori, i clienti, e così via, implementando
in modo particolare alcune
regole, senza però dover fornire alle singole
classi metodi "setter" sulle proprietà comuni. Ad esempio, appunto far
dichiarare alle singole classi figlie "a che livello" verificare l'univocità di
un attributo.
La prima soluzione che avevo adottato consisteva
in un metodo set non-virtuale sulla classe base, per la validazione
delle regole condivise ma anche delle regole specifiche della classe figlia.
Questo possibile poichè la logica di validazione specifica era
concentrata in un metodo virtuale (o astratto), il metodo hook ("gancio") della
GangOfFour, ridefinito da ogni figlia. In sostanza,
una cosa così:
//Account.cs
protected abstract IUniqueChecker UsernameChecker { get;}
public string Username
{
set
{
...
//username univoco. non specifico "a che
livello"
if (UsernameChecker.UsernameExist(value))
{
throw new ArgumentException(...);
}
}
}
//User.cs, estende Account
protected override IUniqueChecker UsernameChecker
{
get
{
//
ritorna l'azienda corrente. in questo modo ho ottenuto
//
il controllo dell'univocità username a "livello di azienda"
}
}
La soluzione cioè prevede
un'interfaccia IUniqueChecker
per la dichiarazione dei metodi di verifica della univocità:
public interface IUniqueChecker
{
bool UsernameExist(string username);
bool MobileExist(string mobile);
}
L'idea di fondo è buona, e infatti è rimasta invariata
nella sostanza. Quello che non mi piaceva era dover implementare negli oggetti
"ambito" di univocità (le aziende, i gruppi, i servizi, cioè tutti gli oggetti
del dominio che "aggregano" utenti) una serie di metodi, quelli dichiarati in
IUniqueChecker, perchè mi pareva che andassero ad
inquinare la loro interfaccia.
Discutibile...
Però così ho colto
l'occasione per applicare un bel pattern: il pattern Command
. L'anima di questa soluzione è quella di raggruppare
tutte le operazioni che devono essere svolte da un comando, e rappresentarle con
un oggetto, invece che con un metodo. Per eseguire poi il comando, si
invoca un metodo specifico sull'oggetto Command, ad esempio
Execute().
Quello che ho fatto perciò è stato convertire
ogni metodo dell'interfaccia di validazione in una classe Command. Ad
esempio: UniqueAccountUsername per verificare l'univocità della username di un
account, UniqueAccountMobile invece per il mobile. Entrambe le
classi implementano l'interfaccia IUniqueCommand,
che definisce un solo metodo, Execute(), per avviare l'operazione di
convalida.
public interface IUniqueCommand
{
bool Execute(string name); //verifica
unicità di name
}
public class UniqueAccountUsername : IUniqueCommand
{
private IList _list;
//ambito
in cui operare
la verifica
public UniqueAccountUsername(IList list) { ... }
public bool Execute(string username)
{
...
return (!found); //univoco se
non trovato
}
}
public class UniqueAccountMobile : IUniqueCommand
{
...
public bool Execute(string mobile) { ... }
}
Quello che manca per chiudere il ciclo è vedere come creare e usare
un oggetto Command. Ecco ad esempio come risulta la validazione nella classe
Account (vedi l'esempio a metà
post):
Ovviamente non ho resistito! Ho dovuto
farlo! Anch'io ho la mia Validation
Library! Ed eccola in tutto il suo (mediocre)
splendore!
//regola, semplice o composta.
//applicabile ad un solo elemento
IConstraint rule = Is.AtLeastLong(1)
.Or(Is.Null())
.And(HasNot.String(" "))
.And(Is.UniqueIn(aUniqueChecker));
//validazione
if (!rule.Eval(aString))
{
...
}
//contesto di validazione.
//convalido regole su elementi diversi
IValidationContext context;
context = Verify.That(aProperty, IsNot.Null(), "property is null")
.And(anotherProperty, rule, "broken rule 1")
.And(aField, anotherRule, new ArgumentException("broken rule 2"));
//validazione
context.Validate();
if (!context.IsValid)
{
...
}
L'ho fatta in una
giornatina di lavoro, con tanto di dormita di riflessione in mezzo (la notte,
santa cosa, offre sempre ottimi consigli...), ma non è solo farina del mio
sacco. Infatti mi è bastato decorare con una serie di Factory la parte delle Constraints di NMock
(God save opensource!). E il risultato è una delle cose che più mi hanno divertito e
soddifatto negli ultimi tempi. Adoro le fluent Api , se non si fosse
capito!
Per oggi è davvero tutto.
Non esistate a commentare! Adoro i confronti costruttivi!
Ciao, a presto.
-papo-