Consider
// a fairly typical Data Access Layer implementation (DAL). have actually
// encountered this on-site [shudders].
public static class DatabaseConnectionFactory
{
public static IDbConnection GetDatabaseConnection ()
{
// 1. get connection string from config
string connectionString = null;
// 2. create connection
SqlConnection connection = null;
connection = new SqlConnection (connectionString);
connection.Open ();
// 3. some other custom stuff
// 4. return connection
return connection;
}
public static IDbCommand GetDatabaseCommand (
string commandString,
IDbConnection connection)
{
// 1. set custom stuff like transaction and timeout
// 2. return command
return new SqlCommand (commandString, connection);
}
}
// embedded DAL logic in business tier - anyone else vomit in their
// mouth just a little? - however what is especially offensive is the
// direct calls to data tier via static class DatabaseConnectionFactory
public class AppointmentBusinessObject
{
public const string CommandString_LoadById_OneParameter =
@"SELECT *
FROM Appointments
WHERE AppointmentId = {0}";
public void LoadById (long appointmentId)
{
// 1. create Sql command string,
string commandString =
string.Format (
CommandString_LoadById_OneParameter,
appointmentId);
try
{
// 2. get connection,
IDbConnection connection =
DatabaseConnectionFactory.
GetDatabaseConnection ();
// 3. get command,
IDbCommand command =
DatabaseConnectionFactory.
GetDatabaseCommand (commandString, connection);
// 4. execute command
Reader reader = command.ExecuteReader ();
// 5. read contents
}
finally
{
// 6. close connection,
if (connection != null)
{
connection.Close ();
}
}
}
}
Now, there are many reasons why this is a poor design, not the least of which is that we are locked into a single connection and a single implementation. We are forced to duplicate this source if any of these parameters need to change - and believe me this has happened!
If this were not reason enough to contemplate a complete revision, our ability to unit test is also impaired. Specifically, we cannot unit test or verify LoadById without hitting a datastore! In fact, we cannot unit test any class or method that invokes LoadById without hitting a datastore.
So what can we do?
Well the first thing to do is identify the actual problem - and here it appears to be a tight coupling between our business and data tiers. If we were to abstract DatabaseConnectionFactory then the result would be a loosely-coupled and more flexible system. To this end, I would suggest introducing an interface (that defines DatabaseConnectionFactory's existing members) and consuming this wherever possible.
For example,
// a simple data store interface, exposes full functional set
// of existing DatabaseConnectionFactory
public interface IDatabaseConnectionFactory
{
IDbConnection GetDatabaseConnection ();
IDbCommand GetDatabaseCommand (
string commandString,
IDbConnection connection);
}
// we want this to be as low-impact as possible, so new
// interface defines members that already exist. sadly,
// static classes cannot implement interfaces directly,
// and so DatabaseConnectionFactory must be made an instance
// class. while this requires a little deft maneuvering,
// this may be accomplished with *zero* impact to existing
// consumers! yay!
//
// 1. remove static key word from class declaration, this makes
// class instance-based
public class DatabaseConnectionFactory : IDbConnectionFactory
{
// 2. remove static key word from members, this simply
// makes existing method implementations instance-based
public IDbConnection GetDatabaseConnection () { ... }
public IDbCommand GetDatabaseCommand (
string commandString,
IDbConnection connection) { ... }
// 3. define *new* static members that delegate to
// instance-based members, this ensures that existing
// consumers do not break
public static IDbConnection GetDatabaseConnection ()
{
// instantiate new factory every call for clarity,
// potential optimization in declaring a static
// lazy-loaded instance member, and delegating
// to that instead
DatabaseConnectionFactory factory =
new DatabaseConnectionFactory ();
return factory.GetDatabaseConnection ();
}
public static IDbCommand GetDatabaseCommand (
string commandString,
IDbConnection connection)
{
DatabaseConnectionFactory factory =
new DatabaseConnectionFactory ();
return factory.GetDatabaseCommand (commandString, connection);
}
}
// embedded DAL logic in business tier - vomitting just a little less -
// less offensive now we reference implementation-independent datastore
// definition
public class AppointmentBusinessObject
{
// still tightly coupled to Sql-compliant datastore
// but manageable.
public const string CommandString_LoadById_OneParameter =
@"SELECT *
FROM Appointments
WHERE AppointmentId = {0}";
// 1. declare new factory member. this is our *dependency*
// it *must* be fulfilled for class to operate successfully
private readonly IDbConnectionFactory _factory = null;
// 2. expose new parameter constructor, permitting consumers
// of *this* class to specify an appropriate datastore
public AppointmentBusinessObject (IDbConnectionFactory factory)
{
_factory = factory;
}
// 3. expose new parameterless constructor, this preserves
// existing consumers who may not be "up to speed" regarding
// this new-fangled connection specification. we also preserve
// previous operating expectations by defaulting to ...
// "default" connection factory, so at worst, we deliver
// *EXACTLY* same behaviour as before
public AppointmentBusinessObject ()
: this (new DatabaseConnectionFactory ())
{
}
// 4. consume!
public void LoadById (long appointmentId)
{
// ...
try
{
IDbConnection connection =
_factory.GetDatabaseConnection ();
IDbCommand command =
_factory.GetDatabaseCommand (commandString, connection);
// ...
}
finally
{
// ...
}
}
}
So, where is the payoff exactly? Well, for one if we now wish to change datastore implementation (say to a MySql, or Oracle, or some other Sql-compliant datastore) we implement a new class and may swap between the two when desired. We also gain the ability to load objects from two or more datastores at the same time!
As a pleasant side-effect, we are also able to unit test LoadById directly, and any consumers that permit datastore specification.
// test business logic without hitting datastore! yay!
[TestMethod]
public void Test_LoadById ()
{
IDbConnectionFactory mockFactory = null;
// instantiate mock with expectations
AppointmentBusinessObject appointment =
new AppointmentBusinessObject (mockFactory);
appointment.LoadById (1024);
// verify results
}