Ogni tanto (IMHO non abbastanza) sui NewsGroup si parla di design e l'argomento principe per chi si sta approcciando al problema è quasi sempre la stratificazione (suddivisione in Layer) delle applicazioni.
Naturalmente il "problema" l'ho dovuto sviscerare anche io e dopo varie reimplementazioni l'ho "rilsolto" (almeno per ora).
Quando sono di fronte ad applicazioni di un certo "spessore" la struttura è sintetizzabile così:
Domain Layer
Rappresenta lo strato di business e contiene tutte le classi/entities necessarie.
Una esempio tipo di business entity(s) potrebbe essere:
//
// ASSEMBLY
// Topics.Common.dll
//
/*
MyObject è la classe di base da cui tutte le
entity del mio framework erditano
*/
public abstract class MyObject
{
protected virtual void Initialize( System.Data.IDataReader reader )
{
if( reader == null )
{
/*
L'istanza rappresenta un nuovo oggetto
quindi inizializziamo i field con i
valori di default
*/
this._uid = Guid.NewGuid();
this._description = String.Empty;
this._isNew = true;
}
else
{
this._uid = reader.GetGuid( reader.GetOrdinal( "UID" ) );
this._description = reader.GetString( reader.GetOrdinal( "Description" ) );
this._isNew = false;
}
}
protected MyObject( System.Data.IDataReader reader )
{
if( reader == null )
{
throw new ArgumentException( "Invalid Source" );
}
this.Initialize( reader );
}
protected MyObject()
{
this.Initialize( null );
}
private Guid _uid;
//La chiave primaria
public Guid UID
{
get{ return this._uid; }
}
private bool _isNew;
/*
La proprietà in sola lettura IsNew
ci permette di sapere se l'istanza
che stiamo maneggiando arriva da un
DataSource o sia una nuovo elemento
*/
public bool IsNew
{
get{ return this._isNew; }
}
private string _description;
public string Description
{
get{ return this._description; }
set
{
if( value == null )
{
value = String.Empty;
}
if( !value.Equals( this._description ) )
{
this._description = value;
}
}
}
/*
ParametersCollection (omessa per semplicità) non è che una
collection di coppia Key (String)/Value (Object) che viene
usata a mo di MOC object al fine di scambiare dati con lo
strato di factory
*/
protected virtual ParametersCollection GetParameters()
{
ParametersCollection parameters = new ParametersCollection();
parameters.Add( "UID", this.UID );
parameters.Add( "Description", this.Description );
return parameters;
}
protected abstract void InsertData( ParametersCollection parameters );
protected abstract void UpdateData( ParametersCollection parameters );
protected abstract void DeleteData( ParametersCollection parameters );
public void Save()
{
/*
Nell'applicazione reale la gestione è decisamente più
complessa e comprende una serie di eventi che permettono
di manipolare dall'esterno alcuni aspetti e consento anche
di interrompere il processo di salvataggio e/o cancellazione
*/
ParametersCollection parameters = this.GetParameters();
/*
Usiamo IsNew per determinare quale sia l'azione
da intraprendere per il salvataggio
*/
if( this.IsNew )
{
this.InsertData( parameters );
}
else
{
this.UpdateData( parameters );
}
}
public void Delete()
{
ParametersCollection parameters = new ParametersCollection();
parameters.Add( "UID", this.UID );
this.DeleteData( parameters );
}
}
//
// ASSEMBLY
// Topics.MyApplication.Domain.dll
//
public class MyCustomer : MyObject
{
protected override void Initialize( System.Data.IDataReader reader )
{
if( reader == null )
{
/*
L'istanza rappresenta un nuovo oggetto
quindi inizializziamo i field con i
valori di default
*/
this._companyName = String.Empty;
}
else
{
this._companyName = reader.GetString( reader.GetOrdinal( "CompanyName" ) );
}
}
public MyCustomer( System.Data.IDataReader reader )
: base( reader )
{
}
public MyCustomer()
: base()
{
}
private string _companyName;
public string CompanyName
{
get{ return this._companyName; }
set
{
if( value == null )
{
value = String.Empty;
}
if( !value.Equals( this._companyName ) )
{
this._companyName = value;
}
}
}
protected override ParametersCollection GetParameters()
{
ParametersCollection parameters = base.GetParameters();
parameters.Add( "CompanyName", this.CompanyName );
return parameters;
}
protected override void InsertData( ParametersCollection parameters )
{
/*
TODO
Solo la classe "reale" Customer conosce il sistema
per persistere/manipolare i propri dati
*/
}
protected override void UpdateData( ParametersCollection parameters )
{
/*
TODO
Solo la classe "reale" Customer conosce il sistema
per persistere/manipolare i propri dati
*/
}
protected override void DeleteData( ParametersCollection parameters )
{
/*
TODO
Solo la classe "reale" Customer conosce il sistema
per persistere/manipolare i propri dati
*/
}
}
Persistence
Layer// ASSEMBLY
// Topics.Common.dll
//
/*
MyObject è la classe di base da cui tutte le
entity del mio framework erditano
*/
public abstract class MyObject
{
protected virtual void Initialize( System.Data.IDataReader reader )
{
if( reader == null )
{
/*
L'istanza rappresenta un nuovo oggetto
quindi inizializziamo i field con i
valori di default
*/
this._uid = Guid.NewGuid();
this._description = String.Empty;
this._isNew = true;
}
else
{
this._uid = reader.GetGuid( reader.GetOrdinal( "UID" ) );
this._description = reader.GetString( reader.GetOrdinal( "Description" ) );
this._isNew = false;
}
}
protected MyObject( System.Data.IDataReader reader )
{
if( reader == null )
{
throw new ArgumentException( "Invalid Source" );
}
this.Initialize( reader );
}
protected MyObject()
{
this.Initialize( null );
}
private Guid _uid;
//La chiave primaria
public Guid UID
{
get{ return this._uid; }
}
private bool _isNew;
/*
La proprietà in sola lettura IsNew
ci permette di sapere se l'istanza
che stiamo maneggiando arriva da un
DataSource o sia una nuovo elemento
*/
public bool IsNew
{
get{ return this._isNew; }
}
private string _description;
public string Description
{
get{ return this._description; }
set
{
if( value == null )
{
value = String.Empty;
}
if( !value.Equals( this._description ) )
{
this._description = value;
}
}
}
/*
ParametersCollection (omessa per semplicità) non è che una
collection di coppia Key (String)/Value (Object) che viene
usata a mo di MOC object al fine di scambiare dati con lo
strato di factory
*/
protected virtual ParametersCollection GetParameters()
{
ParametersCollection parameters = new ParametersCollection();
parameters.Add( "UID", this.UID );
parameters.Add( "Description", this.Description );
return parameters;
}
protected abstract void InsertData( ParametersCollection parameters );
protected abstract void UpdateData( ParametersCollection parameters );
protected abstract void DeleteData( ParametersCollection parameters );
public void Save()
{
/*
Nell'applicazione reale la gestione è decisamente più
complessa e comprende una serie di eventi che permettono
di manipolare dall'esterno alcuni aspetti e consento anche
di interrompere il processo di salvataggio e/o cancellazione
*/
ParametersCollection parameters = this.GetParameters();
/*
Usiamo IsNew per determinare quale sia l'azione
da intraprendere per il salvataggio
*/
if( this.IsNew )
{
this.InsertData( parameters );
}
else
{
this.UpdateData( parameters );
}
}
public void Delete()
{
ParametersCollection parameters = new ParametersCollection();
parameters.Add( "UID", this.UID );
this.DeleteData( parameters );
}
}
//
// ASSEMBLY
// Topics.MyApplication.Domain.dll
//
public class MyCustomer : MyObject
{
protected override void Initialize( System.Data.IDataReader reader )
{
if( reader == null )
{
/*
L'istanza rappresenta un nuovo oggetto
quindi inizializziamo i field con i
valori di default
*/
this._companyName = String.Empty;
}
else
{
this._companyName = reader.GetString( reader.GetOrdinal( "CompanyName" ) );
}
}
public MyCustomer( System.Data.IDataReader reader )
: base( reader )
{
}
public MyCustomer()
: base()
{
}
private string _companyName;
public string CompanyName
{
get{ return this._companyName; }
set
{
if( value == null )
{
value = String.Empty;
}
if( !value.Equals( this._companyName ) )
{
this._companyName = value;
}
}
}
protected override ParametersCollection GetParameters()
{
ParametersCollection parameters = base.GetParameters();
parameters.Add( "CompanyName", this.CompanyName );
return parameters;
}
protected override void InsertData( ParametersCollection parameters )
{
/*
TODO
Solo la classe "reale" Customer conosce il sistema
per persistere/manipolare i propri dati
*/
}
protected override void UpdateData( ParametersCollection parameters )
{
/*
TODO
Solo la classe "reale" Customer conosce il sistema
per persistere/manipolare i propri dati
*/
}
protected override void DeleteData( ParametersCollection parameters )
{
/*
TODO
Solo la classe "reale" Customer conosce il sistema
per persistere/manipolare i propri dati
*/
}
}
E' lo strato che si occupa effettivamente di persistere ( Insert Update e Delete ) i dati, anche qui un esempio è meglio di mille parole:
//
// ASSEMBLY
// Topics.Common.Persistence.dll
//
public sealed class PersistanceEngine
{
private PersistanceEngine()
{
}
private static PersistanceEngine _engine;
/*
Singleton
*/
public static PersistencaEngine Engine
{
get
{
if( _engine == null )
{
_engine = new PersistanceEngine();
}
return _engine;
}
}
private MyCustomerPersistance _myCustomers
public MyCustomerPersistance MyCustomers
{
get
{
if( this._myCustomers == null )
{
this._myCustomers = MyCustomerPersistance.CreateInstance();
}
return this._myCustomers;
}
}
}
public abstract class MyCustomerPersistance
{
public static MyCustomerPersistance CreateInstance()
{
/*
OMISSIS
Legge da file .config quale tipo di
Persistance debba istanziare e da quale
assembly, trami Activator.CreateInstance( ... )
inizializza la classe "reale" corretta
*/
}
public abstract void Update( ParametersCollection parameters );
public abstract void Insert( ParametersCollection parameters );
public abstract void Delete( ParametersCollection parameters );
}
//
// ASSEMBLY
// Topics.MyApplication.SqlPersistence.dll
//
public class MyCustomerSqlPersistance : MyCustomerPersistance
{
public override void Update( ParametersCollection parameters )
{
//Salva su db via StoredProcedure
}
public override void Insert( ParametersCollection parameters )
{
//Salva su db via StoredProcedure
}
public override void Delete( ParametersCollection parameters )
{
//Cancella da db via StoredProcedure
}
}
Quindi sistemando la ns classe MyCustomer nello strato di
business i metodi InsertData, UpdateData e DeleteData avranno tutti un semplice
corpo del tipo// ASSEMBLY
// Topics.Common.Persistence.dll
//
public sealed class PersistanceEngine
{
private PersistanceEngine()
{
}
private static PersistanceEngine _engine;
/*
Singleton
*/
public static PersistencaEngine Engine
{
get
{
if( _engine == null )
{
_engine = new PersistanceEngine();
}
return _engine;
}
}
private MyCustomerPersistance _myCustomers
public MyCustomerPersistance MyCustomers
{
get
{
if( this._myCustomers == null )
{
this._myCustomers = MyCustomerPersistance.CreateInstance();
}
return this._myCustomers;
}
}
}
public abstract class MyCustomerPersistance
{
public static MyCustomerPersistance CreateInstance()
{
/*
OMISSIS
Legge da file .config quale tipo di
Persistance debba istanziare e da quale
assembly, trami Activator.CreateInstance( ... )
inizializza la classe "reale" corretta
*/
}
public abstract void Update( ParametersCollection parameters );
public abstract void Insert( ParametersCollection parameters );
public abstract void Delete( ParametersCollection parameters );
}
//
// ASSEMBLY
// Topics.MyApplication.SqlPersistence.dll
//
public class MyCustomerSqlPersistance : MyCustomerPersistance
{
public override void Update( ParametersCollection parameters )
{
//Salva su db via StoredProcedure
}
public override void Insert( ParametersCollection parameters )
{
//Salva su db via StoredProcedure
}
public override void Delete( ParametersCollection parameters )
{
//Cancella da db via StoredProcedure
}
}
protected override void InsertData( ParametersCollection parameters )
{
PersistanceEngine.Engine.MyCustomers.Insert( parameters );
}
protected override void UpdateData( ParametersCollection parameters )
{
PersistencaEngine.Engine.MyCustomers.Update( parameters );
}
protected override void DeleteData( ParametersCollection parameters )
{
PersistanceEngine.Engine.MyCustomers.Delete( parameters );
}
In questo modo il domain layer accede ad un'interfaccia astratta e non ha la
più "pallida idea" (come è giusto che sia) di quale sia la reale
implementazione dello strato di Persistance. Questo porta un ulteriore vantaggio siccome
lo strato di persistenza viene istanziato dinamicamente tramite
informazioni prese da un file di configurazione è possibile creare un nuovo
strato di persistenza in un nuovo assembly copiarlo nella directory
"bin
" dell'applicazione modificare opportunemente il file di configurazione e
cambiare radicalmente il comportamento dell'applicazione senza che quest'ultima
se ne accorga e soprattutto senza ricompilare.{
PersistanceEngine.Engine.MyCustomers.Insert( parameters );
}
protected override void UpdateData( ParametersCollection parameters )
{
PersistencaEngine.Engine.MyCustomers.Update( parameters );
}
protected override void DeleteData( ParametersCollection parameters )
{
PersistanceEngine.Engine.MyCustomers.Delete( parameters );
}
"Fine prima parte"
.m
-------------------
Have a nice day
Imported comment, original author: Luca Mauri
ReplyDeleteLa suddivisione in layers descritta nell'articolo separa tramite reflection(activatore.createinstance & co) l'implementazione concreata per una determinata sorgente dati dal dominio applicativo e di business.
Nella mia soluzione(descritta in alcuni post sul blog) ho adottato parte della medesima architettura,in particolare per quanto riguarda la parte di abstract factory applicata al DAL, cioè quella crea a runtime la classe concreta di accesso ai dati.
La diversità della mia architettura è nel modello a oggetti definito dalle custom entitites.
In pratica le mie entities sono solo un agglomerato di proprietà e dati in generale.
Un altro layer, che io chiamo "manager", è quello che si occupa di generare tutte le operazioni di scrittura e lettura sulle entità del "dominio".
Senza entrare troppo nei dettagli, ho poi lasciato ad un engine apposito la gestione dei comandi sql per ciascuna entità, definibili anche tramite attributi sui metodi dei vari manager.
Ho fatto largo uso di codesmith per generare tutte le entities ed i manager relativi.
Imported comment, original author: Giuseppe
ReplyDeleteMolto interessante, avevo già discusso con te di questo problema sul newsgroup di microsoft, e come promesso hai scritto l'articolo. C'è un punto che non mi è molto chiaro, il perchè utilizzare un IDataReader come secondo costruttore della classe per istanziare un oggetto esistente, in questo modo non ti leghi troppo alla sorgente sql?
Nel caso di altra sorgente dati, ad esempio file xml, come fai a passare al costruttore un IDataReader?
Imported comment, original author: Mauro Servienti
ReplyDeleteCiao,
attenzione che IDataReader è l'interfaccia di base implementata da tutti i DataReader (così come dal SqlDataReader) nella nuova versione di NET anche la DataTable ha un metodo che ti ritorna un reader (che implemata IDataReader).
Quindi nulla vieterebbe di cotruire un XmlDataReader che faccia quello che serve.
Comunque sia l'osservazione è giusta e continuo a pensarci anche io, sto valutando la possibilità di inseire anche qui una sorta di MOC Objects da sostituire al reader, non ho ancora implementato la cosa ritenendo che la modifica in futuro non dovrebbe essere così dolorosa dato che riguarda solo il "contratto" tra Factory e Domain senza ripercussioni sul resto dell'applicazione
.Mauro
Imported comment, original author: Giuseppe
ReplyDeleteOltre al "contratto" tra Factory e Domain non viene coinvolto anche il layer di UI? Se non ho capito male la classe MyCustomer viene istanziata anche in questo layer, che richiama direttamente il Domain per le operazioni di Insert/Update/Delete, per cui in questo caso viene coinvolto anche questo nella modifica.
Giuseppe
Imported comment, original author: Giuseppe
ReplyDeleteUn'altra cosa, che cos'è di preciso un MOC Objects? hai qualche link da segnalare dove ci sia qualche esempio?