August 2006 - Posts

BO with DAL, hold the DTO

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.