I am working with an application that is composed of several different, disconnected, components and each piece has a dependency on up to three different Data Stores (SQL Server, Doc Storage, BLOB Storage).
The SQL Server connection details are always known at design / deployment time, however the Doc and BLOB storage (both currently in Azure) details are sometimes provided at design time and sometimes provided at run time depending on the specific component I am working with. Since there is consistent cost in using Azure my requirement is to build a pluggable data access layer that, in the event that the organization wanted to move away from Azure, there would be minimal effort in implementing a new data provider. While the solution that I have come up fulfills the requirement, there are some code smells that I am looking to remove, but I am unsure on how I would achieve them (in the cleanest manner possible). Below is a brief explanation of the structure that I have.
Data Mappers
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
}
}
The smell here is obviously Configure(IDictionary<string, object> configValues)
and the reason for it is because:
- My implementation reaches into the configuration system to determine the type that I should be using
- In the event that I was providing connection details at runtime, I needed a way to pass those details into the provider class while pulling its type from the configuration system.
To actually provide instances of these objects to the applications, I wrote a Service Locator as such
Service Locator
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;
}
}
The non-DI way to then get a concrete provider could then be something like
var database = new ProviderLocator().CreateInstance<ISqlProvider>(null);
The Service Locator implements both the locator pattern and provider "pattern" (someone check to see that Mark Seemann hasn't had a stroke ;] ), but despite the compelling arguments that Mark makes against these patterns here and here I am not sure how to come off of this implementation.
The quick answer here is to probably use an Abstract Factory and to remove the dependency on the configuration system.
Abstract Factory
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);
}
}
My two biggest concerns against implementing this pattern are:
- My classes will now have up 3 factory dependencies (one for each data provider); this isn't a huge concern since my DI container will build my object graph but it does add a certain amount of clutter to the class.
- The Abstract Factory violates SOLID if and when I have to change the concrete provider (e.g. SqlDataProvider becomes AzureDataProvider)
TL;DR / Overall Question
My question is: does a pattern exist (or can one of the ones above be modified) that allows me the flexibility I am looking for that isn't as smelly while still being DI friendly?
Aucun commentaire:
Enregistrer un commentaire