We've all seen the classic (and compelling) example of why generics are useful:
// C#
List numbers;
numbers = new List();
numbers.Add(123);
...
Not only is this more efficient (no boxing), but type-safe as well. But as I discussed in my 6/21 webcast ("What's New in .NET 2.0?", http://www.lakeforest.edu/~hummel/webcasts.htm), generics can be used in much more subtle --- and confusing --- ways. Whether this is good or not remains to be seen, but for now I'm just playing around with what else you can do with generics.
For example, I'm a big fan of generic data access code, i.e. ADO.NET code that can easily work against different databases. Even if you never use this code in production, it's a good example of how inheritance and interfaces can be used in real code.
Generics give us yet another option for writing generic data access code. For example, we define a GenericDataAccess class parameterized based on the type of Connection, Command, and DataAdapter objects to use:
public class GenericDataAccess<T1, T2, T3>
{
private string m_ConnectionString;
public GenericDataAccess(string connectionString)
{ this.m_ConnectionString = connectionString; }
public DataSet Retrieve(string sql)
{
T1 dbConnObject;
dbConnObject = new T1(this.m_ConnectionString);
.
.
.
}
// and so on
}
The better way to write this --- to make sure it is used correctly --- is to add constraints to the class decl, like this:
public class GenericDataAccess<T1, T2, T3>
where T1 : System.Data.IDbConnection, new()
where T2 : System.Data.IDbCommand, new()
where T3 : System.Data.IDbAdapter, new()
{ ... }
This ensures that the developer trying to use this GenericDataAccess class passes types that implement the appropriate interfaces for DB access, and support default constructors.
Just for fun, let's look at yet another way to write this --- using generic methods. One advantage is that we'll end up with a single data access object that can simultateously talk to different databases. So the class decl reverts back to an ordinary looking one:
public class GenericDataAccess
{
But then we define the Retrieve method like this:
public DataSet Retrieve<T1, T2, T3>(string connection, string sql)
where T1 : System.Data.IDbConnection, new()
where T2 : System.Data.IDbCommand, new()
where T3 : System.Data.IDbAdapter, new()
{
T1 dbConnObject;
dbConnObject = new T1();
dbConnObject.ConnectionString = connection;
...
}
In other words, the method is generic --- the caller must supply 3 types that control the type of database to execute the SQL against. So how do you call this thing?
using MSA = System.Data.OleDb;
using SQL = System.Data.SqlClient;
DataAccess data = new DataAccess();
DataSet ds1, ds2;
// access a MS Access database...
ds1 = data.Retrieve<MSA.OLEDBCONNECTION, MSA.OleDbCommand, MSA.OleDbDataAdapter>(“connection string”, “Select * ...”);
// now access a SQL Server database...
ds2 = data.Retrieve<SQL.SQLCONNECTION, SQL.SqlCommand, SQL.SqlDataAdapter>(“connection string”, “Select * ...”);
Crazy? Yes. Practical? I don't know, but I have that luxury as an academic :-) I can tell you that the code above runs about as eficiently as generic data access code based on an abstract base class, inheritance, interfaces, and the factory method design pattern. The latter is very elegant, OO, but takes more effort to develop. The generics-style approach is easier to throw together, runs about as fast, and seems to fall more in line with the growing practice of XP and Agile Programming, where you "don't over-design, but design and refactor as you go".
Either way, generics are going to yield some interesting code. And more rope for some of us to hang ourselves with :-)
Posted
Jun 24 2005, 10:56 AM
by
joe-hummel