Como faço para remover o cheiro de um DAL conectável

5

Eu estou trabalhando com um aplicativo que é composto por vários componentes diferentes e desconectados, e cada parte tem uma dependência de até três armazenamentos de dados diferentes (SQL Server, Armazenamento de Documento, Armazenamento BLOB).

Os detalhes de conexão do SQL Server são sempre conhecidos no tempo de design / implantação, no entanto, os detalhes de armazenamento Doc e BLOB (atualmente no Azure) às vezes são fornecidos em tempo de design e às vezes fornecidos em tempo de execução, dependendo do componente específico em que estou trabalhando com. Como há um custo consistente ao usar o Azure, meu requisito é criar uma camada de acesso a dados conectável que, no caso de a organização desejar se afastar do Azure, haveria um esforço mínimo na implementação de um novo provedor de dados. Enquanto a solução que eu submeto preenche o requisito, há alguns cheiros de código que eu estou procurando remover, mas não tenho certeza de como eu os alcançaria (da maneira mais limpa possível). Abaixo está uma breve explicação da estrutura que eu tenho.

Mapeadores de dados

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
     }
}

O cheiro aqui é obviamente Configure(IDictionary<string, object> configValues) e a razão para isso é porque:

  • Minha implementação chega ao sistema de configuração para determinar o tipo que eu deveria estar usando
  • No caso de eu fornecer detalhes de conexão em tempo de execução, eu precisava de uma maneira de passar esses detalhes para a classe do provedor enquanto puxava seu tipo do sistema de configuração.

Para realmente fornecer instâncias desses objetos para os aplicativos, eu escrevi um Service Locator como tal

Localizador de serviços

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;
    }
}

A maneira não-DI de obter um provedor concreto poderia ser algo como

var database = new ProviderLocator().CreateInstance<ISqlProvider>(null);

O Service Locator implementa o padrão de localizador e o "padrão" do provedor (alguém verifica se Mark Seemann não teve um derrame;]), mas apesar dos argumentos convincentes que Mark faz contra esses padrões here e aqui Não sei como sair dessa implementação.

A resposta rápida aqui é provavelmente usar um Abstract Factory e remover a dependência do sistema de configuração.

Fábrica abstrata

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);
    }
}

Minhas duas maiores preocupações contra a implementação desse padrão são:

  • Minhas turmas terão agora 3 dependências de fábrica (uma para cada provedor de dados); isso não é uma grande preocupação, já que meu contêiner DI irá construir meu gráfico de objeto, mas adiciona uma certa quantidade de desordem à classe.
  • O Abstract Factory viola o SOLID se e quando eu tiver que mudar o provedor concreto (por exemplo, SqlDataProvider se torna AzureDataProvider)

TL; DR / pergunta geral

A minha pergunta é: existe um padrão (ou pode ser modificado um dos acima) que me permita a flexibilidade que estou procurando e que não seja tão fedorento ao mesmo tempo que seja amigável com DI?

    
por dparsons 27.10.2016 / 20:10
fonte

1 resposta

2

Coincidentemente, tenho trabalhado em uma situação semelhante ultimamente - longa história: projetar e implementar um Extensão de Processamento de Dados para SSRS que permitiria agregar dados (para relatórios) provenientes de vários tipos de loja: CSV ou JSON localizados no sistema de arquivos ou SQL simples (via System.Data.SqlClient ), ou pontos de extremidade de serviço do WCF, ou algumas interfaces de serviço em processo (através de reflexão padrão), ou algumas outras fontes / formatos mais estranhos.

Eu não tenho IP na fonte, por isso não posso divulgar mais do que o abaixo - se apenas para uma visualização de 20.000 pés, resolvi algo parecido com isto:

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);
}

Então, mais tarde:

[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;
    }
}

Onde, sem surpresa:

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

Depois, mais uma vez:

// 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);
}

Onde, 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
    }
}

Ou também:

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

Até agora, ele está sendo dimensionado bem o suficiente, ou seja, não é muito dele.

Finalmente, embora eu goste deles, não senti a necessidade de introduzir nenhum genérico neste (ainda / se algum).

'Espero que isso ajude.

    
por 04.11.2016 / 07:15
fonte