Sto lavorando con un'applicazione composta da diversi componenti diversi, disconnessi e ogni pezzo ha una dipendenza da fino a tre diversi archivi di dati (SQL Server, Archiviazione di documenti, Archiviazione BLOB).
I dettagli di connessione di SQL Server sono sempre noti al momento della progettazione / distribuzione, tuttavia i dettagli dello spazio di archiviazione Doc e BLOB (entrambi attualmente in Azure) sono talvolta forniti in fase di progettazione e talvolta forniti in fase di esecuzione a seconda del componente specifico su cui sto lavorando con. Poiché i costi per l'utilizzo di Azure sono consistenti, il mio requisito è quello di creare un livello di accesso ai dati collegabile che, nel caso in cui l'organizzazione volesse allontanarsi da Azure, ci sarebbe uno sforzo minimo nell'implementazione di un nuovo fornitore di dati. Mentre la soluzione che ho trovato soddisfa il requisito, ci sono alcuni odori di codice che sto cercando di rimuovere, ma non sono sicuro di come ottenerli (nel modo più pulito possibile). Di seguito una breve spiegazione della struttura che ho.
Mappatori di dati
public interface IBaseProvider
{
void Configure(IDictionary<string, object> configValues);
}
public interface ISqlProvider : IBaseProvider
{
///CRUD omitted for clarity
}
public interface IBlobProvider : IBaseProvider
{
///CRUD omitted for clarity
}
public interface IDocProvider : IBaseProvider
{
///CRUD omitted for clarity
}
public class SqlDataProvider : ISqlProvider
{
public void Configure(IDictionary<string, object> configValues)
{
//Do stuff
}
}
public class DocDataProvider : IDocProvider
{
public void Configure(IDictionary<string, object> configValues)
{
//Do stuff
}
}
public class BlobDataProvider : IBlobProvider
{
public void Configure(IDictionary<string, object> configValues)
{
//Do stuff
}
}
L'odore qui è ovviamente Configure(IDictionary<string, object> configValues)
e il motivo è perché:
- La mia implementazione raggiunge il sistema di configurazione per determinare il tipo che dovrei usare
- Nel caso in cui stavo fornendo i dettagli della connessione in fase di esecuzione, avevo bisogno di un modo per passare quei dettagli nella classe del provider mentre estraevo il suo tipo dal sistema di configurazione.
Per fornire effettivamente istanze di questi oggetti alle applicazioni, ho scritto un Localizzatore di servizio come tale
Localizzatore di servizio
public interface IProviderLocator
{
T CreateInstance<T>(IDictionary<string, object> configValues) where T : IBaseProvider;
}
public sealed class ProviderLocator : IProviderLocator
{
protected IDictionary<string, object> configValues;
public T CreateInstance<T>(IDictionary<string, object> configurationValues) where T : IBaseProvider
{
configValues = configurationValues;
return Initialize<T>();
}
private T Initialize<T>() where T : IBaseProvider
{
//reach into the configuration system to get providerType
var provider = (T)Activator.CreateInstance(providerType);
provider.Configure(configValues);
return provider;
}
}
Il modo non-D per ottenere un fornitore concreto potrebbe quindi essere qualcosa di simile a
var database = new ProviderLocator().CreateInstance<ISqlProvider>(null);
Il Service Locator implementa sia il pattern di localizzazione che il "pattern" del provider (qualcuno controlla che Mark Seemann non abbia avuto un ictus;]), ma nonostante gli argomenti convincenti che Mark fa contro questi pattern qui e qui Non sono sicuro di come uscire da questa implementazione.
La risposta rapida qui è di usare probabilmente una Fabbrica astratta e rimuovere la dipendenza dal sistema di configurazione.
Fabbrica astratta
public interface IProviderFactory<T>
{
T CreateInstance<T>(IDictionary<string, object> configValues)
}
public sealed class SqlProviderFactory : IProviderFactory<ISqlProvider>
{
public T CreateInstance<T>(IDictionary<string, object> configurationValues)
{
return new SqlDataProvider(configurationValues);
}
}
Le mie due maggiori preoccupazioni contro l'implementazione di questo modello sono:
- Le mie classi avranno ora 3 dipendenze di fabbrica (una per ogni fornitore di dati); questa non è una grande preoccupazione dal momento che il mio contenitore DI costruirà il mio oggetto grafico ma aggiunge una certa quantità di confusione alla classe.
- La Fabbrica astratta viola SOLID se e quando devo cambiare il fornitore concreto (ad esempio, SqlDataProvider diventa AzureDataProvider)
TL; DR / Domanda generale
La mia domanda è: esiste uno schema (o può uno di quelli sopra essere modificato) che mi permette la flessibilità che sto cercando che non è così maleodorante pur rimanendo DI friendly?