Generic multi-layer data access pattern?
I've been playing around with some new patterns for n-layer data access, and came across one that seems very flexible and easy to implement. Basically, I needed a solution that could make various data layers pluggable/swapabale on the fly - i.e., base data access from DB, distributed caching开发者_运维问答, local caching, etc.
The code below is easily reused and incredibly efficient - only a few ticks longer than my previous entirely-hard-coded solution.
How does this look? Is there any way that this could be implemented better? Any general thoughts or critiques? Any input from those who have used similar patterns?
Base Class:
public class DataAdapterFactory<T> where T : class
{
private DataAdapter<T> Adapter;
public DataAdapterFactory(DataAdapterBase<T> Base)
{
Adapter = Base;
}
public void Extend<U>() where U : DataAdapterExtender<T>, T, new()
{
DataAdapterExtender<T> Extender = new U();
Extender.BaseAdapter = Adapter as T;
Adapter = Extender;
}
public T GetAdapter()
{
return Adapter as T;
}
}
public class DataAdapter<T> where T : class { }
public class DataAdapterBase<T> : DataAdapter<T> where T : class { }
public class DataAdapterExtender<T> : DataAdapter<T> where T : class
{
public T BaseAdapter;
}
Implementing in the DAL:
// base interface defines methods
public interface IMyDataAdapter
{
string GetString();
}
// base sql adapter
public class SqlDataAdapter : DataAdapterBase<IMyDataAdapter>, IMyDataAdapter
{
public string GetString()
{
return "SQL";
}
}
// provides cache support
public class DistributedCacheExtender : DataAdapterExtender<IMyDataAdapter>, IMyDataAdapter
{
public string GetString()
{
return BaseAdapter.GetString() + ", Distributed Cache";
}
}
// provides local cache support
public class LocalCacheExtender : DataAdapterExtender<IMyDataAdapter>, IMyDataAdapter
{
public string GetString()
{
return BaseAdapter.GetString() + ", Local Cache";
}
}
Accessing Data:
public IMyDataAdapter GetAdapter()
{
// create adapter based on SqlDataAdapter
DataAdapterFactory<IMyDataAdapter> factory = new DataAdapterFactory<IMyDataAdapter>(new SqlDataAdapter());
// implement distributed cache
factory.Extend<DistributedCacheExtender>();
// implement local cache
factory.Extend<LocalCacheExtender>();
return factory.GetAdapter();
}
Using the factory above, any combination of base adapters and extenders (Extend<>() must be called in the order of execution) can be used easily and seamlessly on the fly via the interface, with the business layer knowing nothing of the implementation.
In this case above, calling GetString() would result in "SQL, Distributed Cache, Local Cache". In a real world scenario, the local cache would be called first. If an item isn't there, we'd head over to the distributed cache, and if it isn't there, we'd get it from the DB - and any module could be swapped in or out as needed depending on the obejct, user, etc.
I'd look at the http://en.wikipedia.org/wiki/Decorator_pattern for this - your example would then become something like this:
public interface IMyDataAdapter
{
string GetString();
}
public class SqlDataAdapter : IMyDataAdapter
{
public string GetString()
{
return "SQL";
}
}
public class LocalCacheDecorator : IMyDataAdapter
{
private IMyDataAdapter adapter;
public LocalCacheDecorator(IMyDataAdapter adapter)
{
this.adapter = adapter;
}
public string GetString()
{
return "Local cache, " + this.adapter.GetString();
}
}
What you're doing looks reasonably sensible, although it'd be helpful to see how the classes are related in terms of what namespaces and assembiles they are in.
My experience is around abstracting out the data providers behind interfaces, with the data providers (and sometimes the interfaces) living in a seperate assembly (so: 1 for the BL, 1 for the interfaces and 1 for each of the data providers).
I define the interfaces around business concepts (like IPageDataProvider, ICustomerDataProvider, etc) not data sources (i.e. db tables), and you want to be mindful of the Interface Segeration Principle when you design the interfaces.
I load the desired data provider at runtime, via a factory; this uses the Activator.CreateInstance method. The factory gets it's instructions from config.
So, when I'm wanting to consume data in my business logic I create an instance of the desired interface implementation via the factory (one line of code).
With this approach there's no base class that the providers have to use, but obviously you could use base classes within a data provider if you wanted to.
I came up with a sort of hybrid abstract factory which may serv your purpose, it also hides connection details from developers. It provides a set of connections, in our case we needed 4 in each set. These four would be returned by a 'connection set' factory, there is also a 'custom connection set factory where you can change the connections returned at any time. You use the proxy to access the connection objects. I then access the proxy through a singleton, that way I can set it in my application load event, or global.asmx, then it is very easy to swap over which connections you are using. Though you could swap around a runtime. Hope this helps.
It is specifically written for my scenario, so may be a bit overkill for you?
Note that this is for npgsql, you could easily change it to the standard .data. client base classes, or sqlclent classes...
Public MustInherit Class DBConnectionDetail
'note this abstract class could be an interface if you didn't want these common methods
Protected _conStrBldr As New Npgsql.NpgsqlConnectionStringBuilder
Protected _connection As Npgsql.NpgsqlConnection
Public Sub New()
'Set the connection builder properties in the subclass
End Sub
Friend ReadOnly Property ConnectionStringBuilder() As Npgsql.NpgsqlConnectionStringBuilder
Get
Return _conStrBldr
End Get
End Property
Friend Property Connection() As Npgsql.NpgsqlConnection
Get
Return _connection
End Get
Set(ByVal value As Npgsql.NpgsqlConnection)
_connection = value
End Set
End Property
' Misc properties - information for programmers of higher layers
Public MustOverride ReadOnly Property Description() As String
Public MustOverride ReadOnly Property HostName() As String
Public MustOverride ReadOnly Property IP() As String
Public MustOverride ReadOnly Property Database() As String
End Class
Public Class LocalArchiveConnectionDetails
Inherits DBConnectionDetail
Public Sub New()
_conStrBldr.Host = "localhost"
_conStrBldr.Port = 5432
_conStrBldr.UserName = "usr"
_conStrBldr.Password = "pwd"
_conStrBldr.Database = "archive"
'_conStrBldr.Pooling = True
'_conStrBldr.MinPoolSize = 5
'_conStrBldr.MaxPoolSize = 10
'_conStrBldr.CommandTimeout = 1024
'_conStrBldr.Timeout = 1024
'_conStrBldr.ConnectionLifeTime = 2
End Sub
Public Overrides ReadOnly Property Description() As String
Get
Return "Local Connection to Database"
End Get
End Property
Public Overrides ReadOnly Property Database() As String
Get
Return "archive"
End Get
End Property
Public Overrides ReadOnly Property HostName() As String
Get
Return "local host"
End Get
End Property
Public Overrides ReadOnly Property IP() As String
Get
Return "127.0.0.1"
End Get
End Property
End Class
Public Interface IConnectionFactory
ReadOnly Property GetMasterConnection() As DBConnectionDetail
ReadOnly Property GetWarehouseConnection() As DBConnectionDetail
ReadOnly Property GetArchiveConnection() As DBConnectionDetail
ReadOnly Property GetAuditConnection() As DBConnectionDetail
End Interface
Public Class DBConnectionBuilder
Friend Shared Function GetConnection(ByVal conStrBldr As DBConnectionDetail) As NpgsqlConnection
Return New NpgsqlConnection(conStrBldr.ConnectionStringBuilder.ConnectionString)
End Function
'Friend Shared Function GetConnection(ByVal conStr As String) As NpgsqlConnection
' Return New NpgsqlConnection(conStr)
'End Function
End Class
Public Class LocalConnectionFactory
Implements IConnectionFactory
Public ReadOnly Property GetArchiveConnection() As DBConnectionDetail Implements IConnectionFactory.GetArchiveConnection
Get
Dim dbConnection As New LocalArchiveConnectionDetails
dbConnection.Connection = DBConnectionBuilder.GetConnection(dbConnection)
Return dbConnection
End Get
End Property
Public ReadOnly Property GetMasterConnection() As DBConnectionDetail Implements IConnectionFactory.GetMasterConnection
Get
Dim dbConnection As New LocalMasterConnectionDetails
dbConnection.Connection = DBConnectionBuilder.GetConnection(dbConnection)
Return dbConnection
End Get
End Property
Public ReadOnly Property GetWarehouseConnection() As DBConnectionDetail Implements IConnectionFactory.GetWarehouseConnection
Get
Dim dbConnection As New LocalWarehouseConnectionDetails
dbConnection.Connection = DBConnectionBuilder.GetConnection(dbConnection)
Return dbConnection
End Get
End Property
Public ReadOnly Property GetAuditConnection() As DBConnectionDetail Implements IConnectionFactory.GetAuditConnection
Get
Dim dbConnection As New LocalAuditConnectionDetails
dbConnection.Connection = DBConnectionBuilder.GetConnection(dbConnection)
Return dbConnection
End Get
End Property
End Class
''' <summary>
''' The custom connection factory allows higher layers to decide which connection will be returned by the connection proxy
''' </summary>
''' <remarks></remarks>
Public Class CustomConnectionFactory
Implements IConnectionFactory
Private _archiveConnection As DBConnectionDetail
Private _masterConnection As DBConnectionDetail
Private _warehouseConnection As DBConnectionDetail
Private _auditConnection As DBConnectionDetail
Friend Sub New()
End Sub
Friend Sub New(ByVal masterConnection As DBConnectionDetail, ByVal archiveConnection As DBConnectionDetail, _
ByVal warehouseConnection As DBConnectionDetail, ByVal auditConnection As DBConnectionDetail)
_masterConnection = masterConnection
_archiveConnection = archiveConnection
_warehouseConnection = archiveConnection
_auditConnection = auditConnection
End Sub
Friend Sub SetMasterConnectionDetail(ByVal connectionDetail As DBConnectionDetail)
_masterConnection = connectionDetail
End Sub
Friend Sub SetArchiveConnectionDetail(ByVal connectionDetail As DBConnectionDetail)
_archiveConnection = connectionDetail
End Sub
Friend Sub SetWarehouseConnectionDetail(ByVal connectionDetail As DBConnectionDetail)
_warehouseConnection = connectionDetail
End Sub
Friend Sub SetAuditConnectionDetail(ByVal connectionDetail As DBConnectionDetail)
_auditConnection = connectionDetail
End Sub
Public ReadOnly Property GetArchiveConnection() As DBConnectionDetail Implements IConnectionFactory.GetArchiveConnection
Get
_archiveConnection.Connection = DBConnectionBuilder.GetConnection(_archiveConnection)
Return _archiveConnection
End Get
End Property
Public ReadOnly Property GetMasterConnection() As DBConnectionDetail Implements IConnectionFactory.GetMasterConnection
Get
_masterConnection.Connection = DBConnectionBuilder.GetConnection(_masterConnection)
Return _masterConnection
End Get
End Property
Public ReadOnly Property GetWarehouseConnection() As DBConnectionDetail Implements IConnectionFactory.GetWarehouseConnection
Get
_warehouseConnection.Connection = DBConnectionBuilder.GetConnection(_warehouseConnection)
Return _warehouseConnection
End Get
End Property
Public ReadOnly Property GetAuditConnection() As DBConnectionDetail Implements IConnectionFactory.GetAuditConnection
Get
_auditConnection.Connection = DBConnectionBuilder.GetConnection(_auditConnection)
Return _auditConnection
End Get
End Property
End Class
Public Class DBConnectionsProxy
Private _ConnectionsFactory As IConnectionFactory
Private _CurrentConnectionsFactory As IConnectionFactory
Public Sub New(ByVal connectionFactory As IConnectionFactory)
'check that a connection factory is provided otherwise nothing will work
If connectionFactory Is Nothing Then
Throw New NullReferenceException("You must provide a connection factory")
Else
_ConnectionsFactory = connectionFactory
_CurrentConnectionsFactory = connectionFactory
End If
End Sub
Friend Property ConnectionFactory() As IConnectionFactory
Get
Return _CurrentConnectionsFactory
End Get
Set(ByVal value As IConnectionFactory)
_CurrentConnectionsFactory = value
End Set
End Property
Public ReadOnly Property GetMasterConnection() As Npgsql.NpgsqlConnection
Get
Return _CurrentConnectionsFactory.GetMasterConnection.Connection
End Get
End Property
Public ReadOnly Property GetArchiveConnection() As Npgsql.NpgsqlConnection
Get
Return _CurrentConnectionsFactory.GetArchiveConnection.Connection
End Get
End Property
Public ReadOnly Property GetWarehouseConnection() As Npgsql.NpgsqlConnection
Get
Return _CurrentConnectionsFactory.GetWarehouseConnection.Connection
End Get
End Property
Public ReadOnly Property GetAuditConnection() As Npgsql.NpgsqlConnection
Get
Return _CurrentConnectionsFactory.GetAuditConnection.Connection
End Get
End Property
''' <summary>
''' Reset current connection factory to original connection factory this proxy was instantiated with
''' </summary>
''' <remarks></remarks>
Public Sub ResetConnection()
_CurrentConnectionsFactory = _ConnectionsFactory
End Sub
''' <summary>
''' Changes the master connection returned for the current connection factory
''' </summary>
''' <param name="connectionDetail">Connection information for master database</param>
''' <remarks></remarks>
Public Sub SetMasterConnection(ByVal connectionDetail As DBConnectionDetail)
Me.SetAllConnections(connectionDetail, _CurrentConnectionsFactory.GetArchiveConnection, _
_CurrentConnectionsFactory.GetWarehouseConnection, _CurrentConnectionsFactory.GetAuditConnection)
End Sub
''' <summary>
''' Changes the archive connection returned for the current connection factory
''' </summary>
''' <param name="connectionDetail">Connection information for archive database</param>
''' <remarks></remarks>
Public Sub SetArchiveConnection(ByVal connectionDetail As DBConnectionDetail)
Me.SetAllConnections(_CurrentConnectionsFactory.GetMasterConnection, connectionDetail, _
_CurrentConnectionsFactory.GetWarehouseConnection, _CurrentConnectionsFactory.GetAuditConnection)
End Sub
''' <summary>
''' Changes the warehouse connection returned for the current connection factory
''' </summary>
''' <param name="connectionDetail">Connection information for warehouse database</param>
''' <remarks></remarks>
Public Sub SetWarehouseConnection(ByVal connectionDetail As DBConnectionDetail)
Me.SetAllConnections(_CurrentConnectionsFactory.GetMasterConnection, _CurrentConnectionsFactory.GetArchiveConnection, _
connectionDetail, _CurrentConnectionsFactory.GetAuditConnection)
End Sub
''' <summary>
''' Changes the audit connection returned for the current connection factory
''' </summary>
''' <param name="connectionDetail">Connection information for audit database</param>
''' <remarks></remarks>
Public Sub SetAuditConnection(ByVal connectionDetail As DBConnectionDetail)
Me.SetAllConnections(_CurrentConnectionsFactory.GetMasterConnection, _CurrentConnectionsFactory.GetArchiveConnection, _
_CurrentConnectionsFactory.GetWarehouseConnection, connectionDetail)
End Sub
''' <summary>
''' Sets the current connection factory to a custom connection factory using the supplied connection
''' </summary>
''' <param name="masterConnectionDetail">Connection information for master database</param>
''' <param name="archiveConnectionDetail">Connection information for archive database</param>
''' <param name="warehouseConnectionDetail">Connection information for warehouse database</param>
''' <remarks></remarks>
Public Sub SetAllConnections(ByVal masterConnectionDetail As DBConnectionDetail, _
ByVal archiveConnectionDetail As DBConnectionDetail, _
ByVal warehouseConnectionDetail As DBConnectionDetail, _
ByVal auditConnectionDetail As DBConnectionDetail)
Dim customConnFactory As New CustomConnectionFactory
customConnFactory.SetMasterConnectionDetail(masterConnectionDetail)
customConnFactory.SetArchiveConnectionDetail(archiveConnectionDetail)
customConnFactory.SetWarehouseConnectionDetail(warehouseConnectionDetail)
customConnFactory.SetAuditConnectionDetail(auditConnectionDetail)
_CurrentConnectionsFactory = customConnFactory
End Sub
End Class
精彩评论