Layers

Summary

Purpose

Stack orthogonal application features that access data with increasing levels of abstraction.

Scenario

The previous patterns described strategies for decoupling the concepts of data access details, data model, and object-relational mapping from application code. These concepts are orthogonal because each can be addressed separately without affecting the other. An application's data model is often orthogonal to its data access mechanisms. The data model is the static structure of the data while data access addresses how to move data from the data store to the application code. Altering the data model does not usually require a change in the data model and the inverse is usually true.

Decoupling orthogonal components is almost always a good idea because it grants you freedom to alter them independently. Finer grained components can also be orthogonal. For example, code within a data accessor often addresses two distinct issues - connection management which involves initializing connections and properly pooling them, and generating ADO.NET (SQL) statements for data access. This is very little overlap between the solution for these two issues, so it makes sense to decouple them into separate modules.

Generally stated, software components are orthogonal if they address completely disjointed issues and can be assembled in any order to build an overall solution. However, it is important to note that orthogonality does not preclude dependence. In other words, just because two modules solve to completely different problems, it does not mean that you cannot build one without the other.

The Layers pattern describes how to stack multiple, orthogonal components together to form a robust and maintainable application.  A layer is a set of components that implement a software abstraction. You can stack abstract layers that incrementally decompose a solution down to its ultimate physical implementation. Layers effectively manage the mapping from abstract concepts to a concrete implementation in steps, solving the problem one piece at a time. The following figure illustrates a stack of layers between application logic and physical database access:

The model above can then be generalized as follows:

As with any orthogonal components, it is always a good practice to decouple layers so that they are only dependent on lower level abstractions, rather than on implementation details. When you define an abstraction (interface) for each layer, you can view a single layer implementation as an adapter from one abstraction to another.

Data access code is amenable to building with layers because there are so many orthogonal aspects to it. For example, the following details are candidates for implementation within a separate layer:

Structure & UML

The static structure of the Layers pattern is shown below:

Application code works in terms of Layer 1, the most abstract data access layer. Each concrete layer M implements its interface and delegates calls to the next and less abstract interface M+1. Concrete layer N (not shown) is the lease abstract and interacts directly with the ADO.NET provider. This static structure may be subject to a few variations:

The sequence diagram of the Layers pattern is shown below:

The application code invokes an interface on concrete layer 1 which delegates to an interface on concrete layer 2 and so on until the last concrete layer is reached. In reality, most layered data access implementations are not this simple - for example, it is quite common for a single call to concrete layer M instance to map to multiple calls to the next layer (layer M+1)

Example

Layer 3 is the data accessor layer. IDataAccess interface defines this layer's abstraction using logical database operations and does not expose any SQL or call-level interface semantics at all. This layer is the bottom-most layer and is implemented in terms of the actual ADO.NET provider:

/* LAYER 3 */

public interface IDataAccess
{
    void ExecuteNonQuery( string strConnectio, string strSQL );
    object ExecuteScalar( string strConnectio, string strSQL );
    DataSet ExecuteDataSet( string strConnectio, string strSQL );
    DataReader ExecuteDataReader( string strConnectio, string strSQL );
}

public class DataAccessSQLServer : IDataAccess
{
    /* IDataAccess implementation */
   
void ExecuteNonQuery( string strConnectio, string strSQL )
    { /* Perform required data access using SQL Server ADO.NET Provider */ }

    object ExecuteScalar( string strConnectio, string strSQL )
    { /* Perform required data access using SQL Server ADO.NET Provider */ }

    DataSet ExecuteDataSet( string strConnectio, string strSQL )
    { /* Perform required data access using SQL Server ADO.NET Provider */ }

    DataReader ExecuteDataReader( string strConnectio, string strSQL )
    { /* Perform required data access using SQL Server ADO.NET Provider */ }
}

public class DataAccessOracle : IDataAccess
{
    /* IDataAccess implementation */
   
void ExecuteNonQuery( string strConnectio, string strSQL )
    { /* Perform required data access using Oracle ADO.NET Provider */ }
    
    ...
}

Layer 2 is the active domain object layer that is responsible for mapping physical data to domain (business) objects. This layer delegates all database calls to layer 1 in order to access the database:

/* LAYER 2 */

public interface ICustomerDataAccess
{
    DataRow  GetCustomer( int nID );
    void     DeleteCustomer( int nID );
    void     CreateCustomer( string strFirstName, string strLastName );
}

public class CustomerDataAccess : ICustomerDataAccess
{
    /* ICustomerDataAccess implementation */
    DataRow GetCustomer( int nID )
    {
        // Do some pre-processing, for example creating parameters for a stored procedure
        // that will be called via the data accessor

        // Get data access component Note how we get an interface 'instance' and access the subsequent
        // data access layer via its interface
        IDataAccess access = GetDataAccessor( Accessor.SQLServer );
        DataSet ds = access.ExecuteDataSet( strConnectionString, "spGetCustomer", aParams );

        // Return data
        return ds.Tables[0].Rows[0];
    }

    void DeleteCustomer( int nID )
    {
        // Do some pre-processing

        // Get an interface 'instance' and access the subsequent data access layer via its interface
    }

    void CreateCustomer( string strFirstName, string strLastName )
    {
        /* Same logic as above ... */
    }
}

public interface IOrderDataAccess
{
    void UpdateOrder( int nID );
    void DeleteOrder( int nID );
    void CreateOrder( string strFirstName, string strLastName );

}

public class OrderDataAccess : IOrderDataAcess
{
    /* IOrderDataAccess implementation */

    public void AddOrder( ... )
    {
        // Do some pre-processing, for example creating parameters for a stored procedure
        // that will be called via the data accessor

        // Get data access component Note how we get an interface 'instance' and access the subsequent
        // data access layer via its interface
        IDataAccess access = GetDataAccessor( Accessor.SQLServer );
        access.ExecuteDataSet( strConnectionString, "spAddOrder", aParams );
    }
}

Layer 1 is the business object layer and it is responsible for implementing various business operations. Note how the SubmitOrder function accesses layer 2 functionality (which accesses layer 1 functionality) to retrieve customer and order information:

/* LAYER 1 */

public interface IOrder
{
    void SubmitOrder( ... );
    void CancelOrder( ... );
    void AmendOrder( ... )
}

public class Order : IOrder
{
    /* IOrder implementation */
    void SubmitOrder( int nOrderID, int nCustomerID )
    {
        // Create a new order
        OrderDataAccess obOrder = new OrderDataAccess();
        obOrder.AddOrder( nOrderID );

        // Get customer identified by nCustomerID and link to this order
        CustomerDataAccess obCust = new CustomerDataAccess();
        DataRow roCust = obCust.GetCustomer( nCustomerID );

        ... 

    }

    void CancelOrder( ... ) { ... }
    void AmendOrder( ... )  { ... }
}

Applicability

Use this pattern when:

Strategies / Variants

Consider these strategies when designing with the Layers pattern:

Benefits

Liabilities

Related Patterns