Generally, when an architecture decision has been made to include
Business Objects that utilize a separate dedicated Data Access Layer
(DAL), completely external from that of the Business Object, there is
one of two basic designs we generally see followed for data retrieval.
Either the DAL returns some sort of data structure (hash table, array,
custom data structure), or xml, etc. which is generally serving the
purpose of a Data Transfer Object (DTO), or the business object is
handed back a direct stream (i.e. data reader) which it (the BO) then
uses to populate itself. For this discussion, I'm excluding the cases
of ORM approaches and that of the business object directly
encapsulating data access code.
While the two basic approaches (DTO or returned stream) used between
a BO and DAL are widely prevalent in an awful lot of systems (I too
have taken one of these two approaches in many circumstances) they are
not the only means to accomplish this task. If the data layer is
utilizing a data reader, Its seems a waste to package up its results
into some DTO only to be passed immediately, perhaps in process, to the
BO, then only to be immediately unpackaged by the BO (i.e. mapped on to
the BO's member variables). However, if you have already made a
decision to separate BO and Data Access code, then returning a data
reader, while nice and fast, can have the negative effect of implicitly
coupling your BO back to your data layer in a way that requires your BO
to know how to read it's data off of the stream. In other words, it can
cause the BO to know about things like db column names or ordinals so
that it can read it's data ; i.e. customer.name = dr.GetString(2) or
customer.name = dr.GetString(dr.GetOrdinal("Customer_Full_Name").
If you have already made a decision to separate BO and DAL then it
should be obvious why this decision is implicitly coupling the two back
together. So, if you have made the decision to separate the two, then
separate the two, and only allow the data access mapping stuff (like db
column names) to live where they belong (in this case), in the DAL and
not in the BO. So, that leaves us with choice of instantiating and
populating a DTO in the DAL, and returning this to the BO. Right?
Wrong. There are other ways of getting this data inside the BO, without
a DTO and without coupling the BO back to DAL stuff like DB column
names on data readers.
Delegates and asynchronous callbacks come in very handy for this
kind of work. The basic idea is that the BO calls upon the DAL to fill
some data, but instead of getting back a DTO or stream it gives the DAL
a handle to a method to be called when the DAL has the data available.
The method signature (delegate) can be defined in the DAL (or better
yet the interface which the respective DAL implements) and this allows
you to keep DB like stuff completely in the DAL, and eliminates the
need for explicit DTO creation simply for the purpose of getting the
data from the DAL back into the BO. It also allows the BO to do with
this data as it will, privately as it should. The DAL simply invokes
the callback handle with the predetermined signature, perhaps passing
the data results directly off the data reader, all without creating a
DTO.
Here is an over simplified example of this type of approach:
Public Interface
ICustomerDataAdapter
Delegate Sub DataAdapterHandlerDelegate(ByVal ID As
Integer, ByVal Name
As String, ByVal Addresss As String, ByVal Phone As String)
Sub FillCustomer(ByVal DataAdapterHandler As
DataAdapterHandlerDelegate, ByVal CustomerID As Integer)
End Interface
Public Class CustomerSQLDataAdapter
Implements ICustomerDataAdapter
Public Sub FillCustomer(ByVal DataAdapterHandler As
ICustomerDataAdapter.DataAdapterHandlerDelegate, _
ByVal CustomerID As
Integer) Implements CustomerDataAdapter.FillCustomer
Using cn As
Data.SqlClient.SqlConnection = New
Data.SqlClient.SqlConnection(connectionString)
Using cm As
Data.SqlClient.SqlCommand = cn.CreateCommand
cm.CommandType = Data.CommandType.StoredProcedure
cm.CommandText =
"getCustomer"
Using dr As Data.SqlClient.SqlDataReader =
cm.ExecuteReader
' Here
is the callback into the Business Object
DataAdapterHandler.Invoke(
dr.GetInt32(dr.GetOrdinal("CustomerID")), _
dr.GetString(dr.GetOrdinal("Customer_FullName")), _
dr.GetString(dr.GetOrdinal("Customer_FullMailingAddress")), _
dr.GetString(dr.GetOrdinal("Customer_HomePhone")))
End Using
End Using
End Using
End Sub
End Class
Public Class Customer
' All the business logic, properties, methods, etc.
Private mID As Integer
Private mName As String
' etc.
Private Sub RetrieveCustomerData()
' you probably want an abstracted
factory call to instantiate the
respective Adapter instance
' and not directly instantiate as
here; thus allowing good stuff like
unit test adapters to be
' swapped in and out with real DB
adapters.
Dim da As ICustomerDataAdapter
da = New CustomerSQLDataAdapter
da.FillCustomer(AddressOf
DataAdapterHandler, mID)
End Sub
Private Sub DataAdapterHandler(ByVal ID As Integer,
ByVal Name As
String, ByVal Addresss As String, ByVal Phone As String)
'populate internal fields, such
as:
mID = ID
mName = Name ' etc.
End Sub
To sum up, if you have already taken the effort to separate BO and
DAL, then don't feel that the only way to get data back onto/into the
BO is
via an explicit DTO, and also don't feel that you have to sacrifice
some separation of duties by requiring the BO to know how to get data
directly off of a data reader. In most cases, the decision to use an
explicit DAL DTO can be internal to the respective data adapter when
needed, or based on other architecture decisions.* In the
above example, there is no DTO and the BO doesn't know anything about
db field names. There is an explicit contract that the
CustomerDataAdapter exposes as to the signature of its FillCustomer
method. However, the column names in the DB are all encapsulated in the
specific SQLDataAdapter instance and do not seep into the BO. The DB
column name could change and only
the DAL code that access this field
would need to be modified, not the dataAdapter fill signature. The
delegate signature, and therefore the BO
can remain untouched. Additionally, the BO doesn't need to expose any
public FillMe methods or implement any IFillable interface (both of
which are completely inappropriate IMHO). Instead the BO encapsulates
the
logic of populating its internal fields within its own private method,
and it is opting to pass the address of this method to the data layer.
So, the data layer does its sole responsibility thing (data access) as
it should and doesn't know anything about the BO, or how the BO is
going to use the data in the delegate signature. Additionaly, the BO
doesn't know anything about how the data layer is getting the data
arguments to the handler method, and it is still ultimately in charge.
It privately calls the fill method, and it privately gives the handle
of the appropriate method to handle the results. The data
adapter can be easily swapped out with a unit test data adapter (or for
any other purpose, i.e. multiple DB types) and there is nothing coupled
(including implicit coupling via DB field names used to read data).
* For example, in the code above I included CustomerSQLDataAdapter that
directly used a SQL DataReader, however, there could be another
CustomerXYZDataAdapter that can't directly use a SQL Connection and
Data Reader, and so internally it might utilize a DTO between some
other component and or process, or perhaps a web service. But once it
receives the DTO, or the web service results, it is able
to invoke the same DataAdatperHandler callback.