Vous êtes sur la page 1sur 6

15 Seconds : Strategy for Building a Pluggable Data Access Layer in

.NET
Adam Sills
06/11 / 2002

Nearly every commercial (and non-commercial) application uses some sort of data access technique. Usually this
is some sort of database, be it SQL Server, MySQL, Access, an XML-based database, or simply flat text files.
No matter what data access techniques are used, the application can easily become tied to one specific
technique and make porting the application to use another data access layer cumbersome at best.

ADO (OLEDB/ODBC) and ADO.NET are Microsoft's solution to an independent data access architecture.
While these technologies are incredibly powerful and very effective when used correctly, they still only enable an
application developer to tie his or her application to one specific data store. SQL Server and the
System.Data.SqlClient namespaces in .NET tie an application to SQL Server. This can be remedied by using the
System.Data.OleDb namespaces; however, using stored procedures will limit your application to SQL Server.

Many application developers write their applications for the lowest common denominator. If the developer is
targeting a Windows machine for the application, then the developer typically uses only what is common between
Access and SQL Server. This means stored procedures are not used. SQL Statements that were once simple in
SQL Server are ported and made convoluted to work in Access (things like SELECT TOP, SELECT
@@Identity, etc). If this developer were targeting the application to run on MySQL, FoxPro, SQL Server, or
Access, there would be a lot of additional work and testing on each database to make sure that the features in
one database work correctly in another.

While this solution can be modularized, be based on application-level settings, specify a particular database, and
execute the appropriate code, very infrequently is the architecture of this data access technique truly "pluggable".
These applications are typically targeted at a number of data stores and at the code written for them. Creating
code to access new data stores can often be a heroic task, tracking down all the places where data access is
used and adding yet another possible line of code to execute based on the new data store. Frequently application
developers maintain multiple code bases for the different data access layers, causing a maintenance nightmare if
not done correctly.

The Solution

The end goal of every developer who is creating an application that targets multiple data stores is to make it as
easy as possible to switch seamlessly from one data store to another. While this is often possible, it is not always
possible to have a data access architecture that can be completely customized without needing to modify the
application itself.

The solution is to create a simple abstract factory pattern for data access. Provide a set of standard "in and out"
interfaces to someone developing a data access layer for a new data store. As long as the person developing the
data access layer understands exactly what comes into the layer and exactly what should come back out of it, the
theory is that you should be able to "plug in" a new data layer without the application ever knowing that it's now
pointing to a new source for its data.
An abstract factory is simply a class that delegates object creation to a concrete factory. The application only
knows about the abstract interfaces defined ahead of time that it needs to use to access its data store. The
"pluggable data layer" provides the concrete classes and exposes them as interfaces, so the application never
needs to maintain references to the actual classes that are being used.

Simple User Management Application

From this point on, I will refer to a fictitious user management application called UserStore. UserStore has some
pretty simple goals: be written in C#.NET; store pertinent user information such as name and birth date; store
phone numbers and addresses.

UserStore's object hierarchy is relatively simple:

As you can see, UserStore's classes aren't incredibly complicated. Were this a typical real-world application,
classes would be much more involved than this; however, this is all that is needed for this article. From this
diagram, we can definitely tell a number of points of entry into the database:

Getting a User class's data


Getting the data for the Addresses in a User
Getting the data for the PhoneNumbers in a User
Adding and Removing PhoneNumbers
Adding and Removing Addresses
Persisting data to the database for every class

The Abstract Factory Interface

The goal of the pluggable data layer is (as the name explicitly states) to be able to plug one set of data code in at
any time without changing the code. This is accomplished by creating a set of interfaces and one class that
contains nothing but a static method. The concept is relatively simple; create a set of predefined interfaces (these
can also be abstract classes, but I prefer interfaces for this specific instance) that your business classes will call
to, but without knowledge of the exact type of class that it is creating. How is this done? Take a look at the
following code:

public interface IUserStoreFactory {}

public class UserStoreDataFactory


{
public static IUserStoreFactory ConcreteDataClass()
{
Type type = Type.GetType(
Configuration.AppSettings["ClassType"]
);
return (IUserStoreFactory)
Activator.CreateInstance(type, null);
}
}

There are a couple of things going on that need explaining. First there is an interface, IuserStoreFactory, which
represents the interfaces necessary to create concrete data classes. This will be described later. Second, there is
a class, UserStoreDataFactory, which has a single static method, ConcreteDataClass, that returns an
IuserStoreFactory. Without looking at the details of the code, you should be able to understand that this
method will somehow magically create an instance of the concrete data factory class and return it as an
IuserStoreFactory.

The details of the ConcreteDataClass method should be relatively simple to understand. First off, a System.Type
object is retrieved using an appSettings entry which is defined in an application configuration file. Simply create a
configuration file and indicate which class to use:

<configuration>
<appSettings>
<add key="ClassType"
value="YourNamespace.YourClass,YourAssembly" />
</appSettings>
</configuration>

From that, a System.Type object is created and passed to Activator.CreateInstance to create an instance of
your class to be used as an IuserStoreFactory.

Some people like to create a separate method for every specific type of concrete factory being created. That is
to have a "CreateSqlDataFactory()" and "CreateAccessDataFactory()". While these are valid approaches, it
limits the developer to only [x] number of predefined sets of classes they can instantiate. Retrieving the type of
class from a configuration file means having your code automatically understand a brand new class factory
without explicitly calling one of the more specific methods.

Now that the UserStoreDataFactory class is created, and we understand how it returns an instance of the
concrete classes, let's take a look at some more interfaces that our application will use:

public interface IUserStoreFactory


{
IUserData GetUserData();
IAddressesData GetAddressesData();
IPhoneNumbersData GetPhoneNumbersData();
}
public interface IUserData
{
void RetrieveUserData(int uniqueID, out string lname,
out string fname, out DateTime birthDate);
void SaveUserData(int uniqueID, string lname,
string fname, DateTime birthDate);
}
public interface IAddressesData
{
DataSet RetrieveAddresses(int userid);
SaveAddress(...);
}
public interface IPhoneNumbersData
{
DataSet RetrievePhoneNumbers(int userid);
SavePhoneNumber(...);
}

First, notice that IuserStoreFactory has been expanded to contain methods for retrieving specific data classes.
Each method returns an instance of a class that implements a given interface that is used to retrieve and set data
in an implementation-independent manner. For instance, IUserData.RetrieveUserData requests that a uniqueID
be passed to it, then sends back (in the form of out parameters) the data for the given user. You should also
notice the methods on IaddressesData and IphoneNumbersData that return DataSets. Since a DataSet is a
database-independent data structure, it's useful as a generic store for data, provided that the developer of the
business objects and the developer of the data classes understand what needs to come out of the DataSet. For
addresses, the DataSet needs a single table with zero or more rows and 5 columns (Street1, Street2, City, State
and PostalCode). For phone numbers, the DataSet needs a single table with zero or more rows and 4 columns
(CountryCode, AreaCode, Prefix and Number). As long as those assumptions are made and documented and
the data classes follow the rules, using a DataSet is a perfect way to transfer data in an implementation-
independent manner.

Creating Concrete Factory and Data Classes

All of this is good in theory; however, it's only as good as my word until you can see how it is implemented.
Imagine that we wanted to use SQL Server 2000 to power UserStore application. What we would do is create
a set of classes (4 in particular) that implemented our interfaces defined above. One class (SqlUserStoreFactory)
would implement IUserStoreFactory, and then we would have 3 other classes that implemented the other 3
interfaces (SqlUserData, SqlAddressesData and SqlPhoneNumbersData). The SqlUserStoreFactory class
would implement the 3 methods on the IUserStoreFactory interface and return the appropriate SQL data class.
Here is some sample code:
namespace squishyWARE.UserStore
{
public class SqlUserStoreFactory : IUserStoreFactory
{
public IUserData GetUserData()
{
return new SqlUserData();
}
public IAddressesData GetAddressesData()
{
return new SqlAddressesData();
}
public IPhoneNumbersData GetPhoneNumbersData()
{
return new SqlPhoneNumbersData();
}
}
}

You can see here how the factory class is responsible in creating the appropriate data class when it is requested.
Your business object would simply call UserStoreDataFactory. ConcreteDataClass() to get an instance of
your SqlUserStoreFactory, then call the appropriate method to get an instance of one of the data-manipulating
classes (SqlUserData, SqlAddressesData, SqlPhoneNumbersData) and call methods on those classes (which
are defined in the interfaces) to get and set data.

Now imagine that you wanted to add a new data provider that your application can use, be it Access, MySQL,
XML, .NET MyServices, or anything else you can think of. All you would need to do is create a new set of data
classes that implemented your defined interfaces and then "register" the factory class in your configuration file.
Once you do that, the UserStore application is using a brand new data source, and it never knew the difference.
Because you defined a set of interfaces for all of your data manipulation and only ever reference the interfaces
themselves and not the implementations of your interfaces, you can truly plug in a brand new data source.

Conclusion

What you have seen here is a way to create a data access layer that is abstracted to a level where the application
and business objects never realize how the data access is implemented. All data access is handled through a set
of defined interfaces that your business objects can work with, and you can be free to implement any number of
data access layers without ever needing to modify your application code.

FAQ

Q: What about speed? Won't dynamically creating these objects at runtime impact performance?
A: It will, but not significantly. The .NET runtime has in it what I like to call "magic". Well not really, but it does
have speed on its side. Activator.CreateInstance is definitely what I would call a slow way to construct an
object. After the object is constructed, the speed of using the interfaces is just like any other interface.

Q: If I create my classes and compile them, where do I need to place the assembly?
A: Your concrete data class assembly needs to be somewhere that the .NET runtime can find it. For private
deployment it will need to be in the bin folder of your application, or in a subfolder if you have set up probing in
your configuration file. You can also optionally install your assembly into the GAC (Global Assembly Cache)
provided you have given it a strong name.

Q: What if I already have my own set of data classes, such as the Microsoft Data Access Application
Block?
A: That's perfectly fine. One of the best things about this technique is that it integrates seamlessly with other data
access techniques. Accessing your data provider can occur any way you want it to; this technique simply
abstracts the data access to a level where your application itself never needs to know how you implemented it.
Implement this technique, then on your SQL Server or Access data layer you can use your other data access
code to actually access the data store.

Q: Are there any applications out there that use this technique? If so, where can I find one to see
some examples?
A: Many companies actually use this technique; however the actual implementation can vary from application to
application. One application that you can find this specific implementation is squishyFORUMS.
squishyFORUMS can be downloaded at http://www.surrealization.com. The download package contains the
PluggableDataAccess code and a SQL Server 2000 implementation of the data access interfaces.

About the Author

Adam Sills is a Software Architect working at GreatLand Insurance, a subsidiary of Kemper Insurance. Adam
specializes in Microsoft .NET applications, and has five years of professional experience working with Microsoft
technologies.

In addition to professional work, Adam created and maintains squishyFORUMS, a product under the company
name squishyWARE ( http://www.squishyweb.com). squishyFORUMS is a multi-use forum object library
targeted at developers. squishyFORUMS powers a small number of high-traffic sites, in addition to his personal
weblog (http://www.surrealization.com). Techniques used in squishyFORUMS was the driving force behind this
article.

Adam can be reached at asills@squishyweb.com

Back to article

Copyright 2005 Jupitermedia Corp. All Rights Reserved.


Legal Notices, Licensing, Reprints, & Permissions, Privacy Policy.
http://www.internet.com

Vous aimerez peut-être aussi