I ran into an architectural problem after I developed a generic library class, SQLDbConn, that connects to a SQL database given a connection string. It implements an interface IDbConn that can also be used to implement a similar class for MS Access or other databases. An instance of SQLDbConnis created by the application via a Factory function:
static public IDbConn MakeDatabaseConnFactory
(DatabaseConnType eType)
After a databse connection is made through this "Connection" object, the nuts and bolts of accessing the database and retrieving rowsets is done using an application-specific "Model" object, defined by an IDbModel interface. For a SQL connection, that would be an instance of the SQLDbModel class.
Here's the design problem I encountered: the application shouldn't have any hard-coded knowledge of what kind of database it is using. It only has a reference to its IDbConnobject after calling the Factory function above. So, it needs to instantiate its Model object without knowing it will actually be a SQLDbModel object. I don't want to put the logic in SQLDbConnto create it, because that Connection class is generic, so shouldn't know about the application-specific Model class. I also don't want to simply create another Factory function - though that would work - because I have already created a database-type aware object - the Connection object - that knows what kind of database is in use. So, how do I get the generic Connection object (SQLDbConn) to create the specific Model object for me, and still remain application neutral?
The solution I came up with uses the VISITOR design pattern. I added the VISITOR pattern's accept method to the IDbConn interface:
bool AcceptVisitor(IDbConnVisitor visitor);
This allows my application to generically extend the connection object's functionality after its instantiation. The IDbConnVisitor interface referenced above specifies a visitmethod for each type of supported database, but does not dictate what that method does (they could order sandwiches from the deli):
public interface IDbConnVisitor
{
bool VisitMsAccess(IDbConn dbConn);
bool VisitSqlOle(IDbConn dbConn);
bool VisitSqlDa(IDbConn dbConn);
}
Since each instance of a Connection object (like SQLDbConn) knows what kind of database it is, it can call its appropriate visit function, without knowing what that function actually does. For the SQLDbConnclass, its acceptfunction looks like this:
public bool AcceptVisitor(IDbConnVisitor visitor)
{
try
{
return visitor.VisitSqlDa(this);
}
catch (System.Exception excp)
{
_errorText = excp.Message;
}
return false;
}
An MS Access Connection object's AcceptVisitor would similarly call VisitMsAccess.
My application then defines an implementation of IDbConnVisitor via a new class, such as CreateDbModelVisitor, whose job is to provide functions that will instantiate the appropriate Model object for any of the supported database types. Inside, it has a property of type IDbModel that the visitor methods instantiate:
public class CreateDbModelVisitor : IDbConnVisitor
{
...
protected IDbModel _model = null;
...
public bool VisitMsAccess(IDbConn dbConn) { ... }
public bool VisitSqlOle(IDbConn dbConn) { ... }
public bool VisitSqlDa(IDbConn dbConn) { ... }
public IDbModel Model { get { return _model; } }
}
For the SQL database, the visitmethod implementation within CreateDbModelVisitoris as follows:
public bool VisitSqlDa(IDbConn dbConn)
{
_errorText = "";
try
{
if (dbConn.IsConnected)
{
_model = new SqlDbModel(dbConn);
return true;
}
else
{
_errorText = "Database has not been connected."
}
}
catch (System.Exception excp)
{
_errorText = excp.Message;
}
return false;
}
Putting it all together, the application would do the following:
IDbConn dbConn = MakeDatabaseConnFactory(config.DbType);
IDbConnVisitor dbVisitor = new CreateDbModelVisitor();
if (dbConn.AcceptVisitor(dbVisitor))
{
IDbModel model = dbVisitor.Model;
...
// make Model calls here to access database
}
Note that the only thing the application knows of its database type is a value that it picks up from a config file.
By using a VISITOR design pattern, the database-specific Connection object (IDbConn), which is supposed to be application neutral, can execute an application-specific function (via AcceptVisitor) for that particular type of database, without knowing the details of what that function does. It just calls the accept method, and that's that.