Come rimuovere l'odore da un DAL collegabile

5

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?

    
posta dparsons 27.10.2016 - 20:10
fonte

1 risposta

2

Per coincidenza, ultimamente ho lavorato in una situazione simile - per farla breve: progettare e implementare un Estensione dell'elaborazione dei dati per SSRS che consente di aggregare i dati (per i report) provenienti da numerosi tipi di archivio: CSV o JSON che si trovano sul file system o SQL semplice (tramite System.Data.SqlClient ), o endpoint di servizio WCF, o alcune interfacce di servizio in-process (tramite riflessione standard), o alcune altre fonti / formati più strani.

Non ho IP sulla sorgente, quindi non posso rivelare più di quanto segue - se solo per una vista di 20.000 piedi, ho optato per qualcosa che assomiglia a questo:

public class DataLocatorFactory
{
    public IDataLocator GetDataLocator(string dataUri)
    {
        // After parsing our more-or-less standard or custom URI scheme,
        // use reflection over custom attribute on DataLocator classes
        // to find the IDataLocator implementation responsible for that sort
        // (read: syntax) of dataUri and instantiate it
    }
}

public interface IDataLocator
{
    IDataProvider GetDataProvider(string dataUri);
}

Quindi, più avanti:

[DataLocator("file")] // for file://<file path> URIs
public class FileDataLocator : DataLocatorBase
{
    // Can create CsvDataTableProvider, JsonDataTableProvider, etc... (all DataTable-centric)
}

[DataLocator("http")] // for http://... URLs
[DataLocator("https")]
public class WebDataLocator : DataLocatorBase
{
    // Can create WcfDataProvider, etc... (not DataTable-centric)
}

[DataLocator("sql")] // for sql://<connection name or string> URIs
// knows only about a single DataTable-centric provider, SqlDataTableProvider
public class SqlDataLocator : DataLocatorBase
{
    public override IDataProvider GetDataProvider(string dataUri)
    {
        if (string.IsNullOrEmpty(dataUri))
        {
            throw new ArgumentException("cannot be null or empty", "dataUri");
        }
        var provider = CreateProvider();
        // Here, we're just caching the DataProviders into a ConcurrentDictionary
        // (map from data URI to the provider thereof)
        // If the freshly created provider can't be bound to dataUri,
        // we just throw it away and return the provider already in cache for that dataUri;
        // of course, that means the DataProvider constructor cannot be resource acquisition- or computation-intensive,
        // but the call to Configure may be, as in WcfDataProvider creating a WS client on the fly,
        // while SqlDataTableProvider simply caches the dataUri/connection string
        // in an instance member
        if (!_dataProviders.TryAdd(dataUri, provider))
        {
            provider = _dataProviders[dataUri];
        }
        else
        {
            // A specific data locator knows how to downcast the IDataProvider into
            // its specific data provider and configure it depending on dataUri and
            // possibly from other info coming from its own configuration, or the call context, or etc
            ((SqlDataTableProvider)provider).Configure(dataUri, ...); // relies on a lock(...) { if (!_configured) { ... } } internally
        }
        return provider;
    }
}

Dove, non sorprendentemente:

public interface IDataProvider
{
    IDataOperation GetOperation(string query);
}

Quindi, più tardi:

// Clients interested in getting their data in the form of System.Data.DataTable,
// but not especially (or at all) interested in knowing how the data is being queried
// and/or from which type of store it will come from, will just go like:
//
// var locator = new DataLocatorFactory().GetDataLocator(uri); // "opaque" URI
// var provider = locator.GetDataProvider(uri);
// if (provider is IDataTableProvider)
// {
//     // Nice, I can work with that provider!
//     var operation = provider.GetOperation(query); // "opaque" query
//     var dataTable = ((IDataTableProvider)provider).GetData(operation, parameters); // "opaque" operation and params
//     // etc...
// }
// else
// {
//     // Bummer!
// }
public interface IDataTableProvider : IDataProvider
{
    DataTable GetData(IDataOperation operation, object[] parameters);
}

Dove, naturalmente:

public class SqlDataTableProvider : DataProviderBase, IDataTableProvider
{
    public override IDataOperation GetOperation(string query)
    {
        // Parse query and create our own dog food here, as, say,
        // a SqlOperation
    }

    public virtual DataTable GetData(IDataOperation operation, object[] parameters)
    {
        // Attempts to downcast operation into, say, a SqlOperation, and if satisfied,
        // interprets it accordingly to emit SQL, run it, and shape the results
        // into the promised DataTable
    }
}

Oppure, anche:

public class JsonDataTableProvider : DataProviderBase, IDataTableProvider
{
    public override IDataOperation GetOperation(string query)
    {
        // Parse query and create our own dog food here, as, say,
        // a JsonPathQuery
    }

    public virtual DataTable GetData(IDataOperation operation, object[] parameters)
    {
        // Attempts to downcast operation into, say, a JsonPathQuery, and if satisfied,
        // interprets it accordingly to delegate the job to a JsonPath implementation,
        // in some JSON parser, and shape the results into the promised DataTable
    }
}

Etc, etc

Finora, è stato ridimensionato abbastanza bene, in linea di principio, cioè (cioè, non troppo di esso).

Infine, anche se mi piacciono, non ho sentito il bisogno di introdurre alcun genere di generici, su quello (ancora / se mai).

'Spero che questo aiuti.

    
risposta data 04.11.2016 - 07:15
fonte

Leggi altre domande sui tag