Domain Object Assembler

Summary

Purpose

Populates, persists, and deletes domain objects using a uniform factory framework.

Scenario

As applications create new domain objects and read their attributes, their domain object mapping implementations issue the analogous database read operations. Similarly, when applications change domain object attributes, the mappings update the database accordingly. Selection Factory, Update Factory and Domain Object Factory describe factory interfaces that address isolated aspects of implementing a domain object mapping. The Domain Object Assembler pattern combines these factories into a fully functioning and customizable domain object mapping framework. A domain object assembler acts as the access point for clients and hides all underlying mapping and data access details. For example:

 

Structure & UML

The following figure illustrates the static structure for the domain object assembler pattern :

The DomainObjectAssembler class defines an entry point for all domain object mapping operations. Client code uses a DomainObjectAssembler instance to initiate read, write, and delete operations. DomainObjectAssembler references a set of pluggable factories that it uses to delegate any implementation features that are specific to a particular domain object type or database entity. ISelectionFactory, IUpdateFactory, and IDomainObjectFactory define these factories generically, while concrete factory implementations fill in the details at runtime.  The DomainObjectAssembler class also manages all database interactions through a DataAccessor. This pattern defines a clear decoupling between generic domain object mapping infrastructure and that which is specific to a particular domain object type or database entity.  

The following figure illustrates the sequence diagram for the domain object assembler pattern:

When a client calls the Read operation on a DomainObjectAssembler instance, the DomainObjectAssembler uses its assigned SelectionFactory implementation to create a selection based on the input identity object. The DomainObjectAssembler then issues a database read operation to read data according the selection statement generated by the SelectionFactory, and then passes each row of the result data to its DomainObjectFactory implementation to instantiate a new domain object. Finally, the DomainObjectAssembler returns the domain object to the client.

When a client calls the Write operation on a DomainObjectAssembler instance, the DomainObjectAssembler uses its assigned SelectionFactory implementation to create a selection based on the input identity object. This selection identifies the existing row to be updated if any. Some DomainObjectAssembler implementations then issues a database read operation to determine whether the corresponding data already exists (this may be necessary to determine whether to issue an INSERT or UPDATE statement. This additional read may degrade performance and the solution usually involves incorporating a persistence state flag in every domain object to determine whether this domain object was assembled from an existing database row or whether the application created this domain object.)

Finally, the DomainObjectAssembler uses its UpdateFactory implementation to create an Update object based on the input domain object's changed attributes. DomainObjectAssembler then issues a database write operation to ensure that database data is updated.

Example

In this example, the DomainObjectAssembler interacts directly with the database using ADO.NET. It represents selections using SQL WHERE clause strings while update operations are represented using maps (hashtables) that contain the names and values of columns to update.

/* When a client initializes a DomainObjectAssember, it is responsible for plugging in specific factory implementations, a database connection string, and the name of the table where data resides */
public class Assembler
{
    // Data members
   
private string                 strConnection = "";
    private string                 strTableName = "";
    private ISelectionFactory      obSelectionFactory = null;
    private IUpdateFactory         obUpdateFactory = null;
    private IDomainObjectFactory   obDomainObjectFactory = null;

    // Save parameters
   
public Assembler(string conn, string table, ISelectionFactory sel,
    IUpdateFactory upd, IDomainObjectFactory dof)
    {
        strConnection         = conn;
        strTableName          = table;
        obSelectionFactory    = sel;
        obUpdateFactory       = upd;
        obDomainObjectFactory = dof;
    }

    // Public interface

    // Read domain objects that correspond to the given identity object
   
public ArrayList Read(object obIdentity)
    {
        // Get the selection clause
       
string strWhereClause = obSelectionFactory.NewSelection( obIdentity );;

        // Execute query against database
       
string strSQL = "select * from " + strTableName + strWhereClause;
        DataTable dt = ExecuteSQL( strSQL );

        // Iterate through the result set and create a domain object for each row
        ArrayList alDomainObjects = new ArrayList();
        foreach (DataRow row in dt.Rows)
        {
            // Use the domain object factory to create a domain object from this row
            alDomainObjects.Add( obDomainObjectFactory.NewDomainObject( row ) );
        }
        return alDomainObjects;
    }

    // Write a domain object that corresponds to the given identity object
   
public void Write(object obIdentity)
    {
        // Query database table to see if any row matches the suppled identity object
        // This is used to determine whether an UPDATE or an INSERT is required
       
string strWhereClause = obSelectionFactory.NewSelection( obIdentity );
        string strSQL = "select * from" + strTableName + strWhereClause;
        DataTable dt = ExecuteSQL( strSQL );
        string strSQLAction = (dt.Rows.Count > 0)? "UPDATE" : "INSERT";

        // Use the update factory to generate a map of updatable values
        Hashtable htUpdateMap = obUpdateFactory.NewUpdate( obIdentity );

        // Loop over the map and use strSQLAction (and strWhereClause if necessary) to generate
        // the update/insert SQL statement
        // ...

        // Finally, issue the update/insert operations
        ExecuteSQL( strSQL );
    }

    // Delete domain objects that correspond to the given identity object
   
public void Delete(object obIdentity)
    {
        // Get the selection clause and construct a DELETE statement
       
string strWhereClause = obSelectionFactory.NewSelection( obIdentity );
        string strSQL = "DELETE from T " + strWhereClause;

        // Issue the delete statement
        ExecuteSQL( strSQL );
    }

    // Helper
    private DataTable ExecuteSQL( string str ) { ... }
}

All the sample code has been generic - it works entirely in terms of ISelectionFactory, IUpdateFactory and IDomainObjectAssembler. The code does not make any reference to any to domain object types or database entities. The next set of classes define the mapping details for a specific domain object type. The code that follows deals with a [DEPOSIT_ACCOUNT] table and a DepositAccount object. The DepositAccount class defines the primary domain object in this example. Each DepositAccount object (i.e., instance) corresponds to a row of data in the [DEPOSIT_ACCOUNT] table. There is also a DepostiAccountKey class whose instances correspond to the primary key values for the table. DepositAccountKey acts as the identity object for this example.

public class DepositAccount
{
    // Data members
   
private string m_strAccountID = "";
    private string m_strEmployeeID = "";
    private string m_strBankName = "";
    private string m_strReference = "";

    // Constructor
   
public DepositAccount(string eid, string aid, string bank, string reference )
    {
        m_strEmployeeID = eid;
        m_strAccountID = aid;
        m_strBankName = bank;
        m_strReference = reference;
    }

    // Properties
   
public string AccountID
    {
        get { return m_strAccountID; }
        set { m_strAccountID = value; }
    }
    public string EmployeeID
    {
        get { return m_strEmployeeID; }
        set { m_strEmployeeID = value; }
    }
    public string BankName
    {
        get { return m_strBankName; }
        set { m_strBankName = value; }
    }
    public string Note
    {
        get { return m_strReference; }
        set { m_strReference = value; }
    }
}

public class DepositAccountKey
{
    // Data members
   
private string m_strAccountID = "";
    private string m_strEmployeeID = "";

    // Constructor
   
public DepositAccountKey(string eid, string aid)
    {
        m_strEmployeeID = eid;
        m_strAccountID = aid;
    }

    // Properties
   
public string AccountID
    {
        get { return m_strAccountID; }
        set { m_strAccountID = value; }
    }
    public string EmployeeID
    {
        get { return m_strEmployeeID; }
        set { m_strEmployeeID = value; }
    }
}

The classes that follow define the factory implementations that plug into a DomainObjectAssembler instance to customize it for mapping DepositAccount and DepositAccountKey instances to the contents of the [DEPOSIT_ACCOUNT] table. Together these three classes make up a single extension of the DomainObjectAssmbler's framework: 

public interface ISelectionFactory
{
    string NewSelection ( object obIdentityObject );
}

public interface IUpdateFactory
{
    Hashtable NewUpdate ( object obIdentityObject );
}

public interface IDomainObjectFactory
{
    ArrayList GetColumnNames();
    object NewDomainObject( DataRow row );
}

// This class implements ISelectionFactory and is responsible for translating a DepositAccountKey
// identity object to a physical selection in the form of a SQL WHERE clause
public class DepositAccountSelectionFactory : ISelectionFactory
{
    // ISelectionFactory implementation
   
public string NewSelection ( object obIdentityObject )
    {
        // Get the deposit account key object
        DepositAccountKey key = (DepositAccountKey)obIdentityObject;

        StringBuilder sb = new StringBuilder();
        sb.AppendFormat( " WHERE EMPLOYEE_ID = {0} and ACCOUNT_ID = {1}",
        key.EmployeeID, key.AccountID );
        return sb.ToString();
    }
}

// This class implements IDomainObjectFactory and is responsible for translating a row
// of data from the [DEPOSIT_ACCOUNT] table to a new DepositAccount object
public class DepositAccountFactory : IDomainObjectFactory
{

    // IDomainObjectFactory implementation
   
public object NewDomainObject( DataRow row )
    {
        // Collect data from the supplied data row
       
string strEmployeeID = (string)row["EMPLOYEE_ID"];
        string strAccountID  = (string)row["ACCOUNT_ID"];
        string strBankName   = (string)row["BANK_NAME"];
        string strReference  = (string)row["REFERENCE"];

        // Create a new domain object based on data collected from the data row and return new object
    
    DepositAccount da = new DepositAccount(strEmployeeID, strAccountID, strBankName, strReference);
        return da;
    }

    public ArrayList GetColumnNames()
    {
        ArrayList alColumns = new ArrayList();
        alColumns.Add( "EMPLOYEE_ID" );
        alColumns.Add( "ACCOUNT_ID" );
        alColumns.Add( "BANK_NAME" );
        alColumns.Add( "REFERENCE" );

        return alColumns;
    }
}

// This class implements IUpdateFactory that is responsible for generating a map
// of columns names and column values that. This map represents the domain object's
// attributes in terms of the [DEPOSIT_ACCOUNT] table
p
ublic class DepositAccountUpdateFactory : IUpdateFactory
{
    // IUpdateFactory implementation
   
public Hashtable NewUpdate ( object obIdentityObject )
    {
        // Get the deposit account key object
        DepositAccount obAccount = (DepositAccount)obIdentityObject;

        Hashtable map = new Hashtable();
        map.Add( "EMPLOYEE_ID", obAccount.EmployeeID );
        map.Add( "ACCOUNT_ID", obAccount.AccountID );
        map.Add( "BANK_NAME", obAccount.BankName );
        map.Add( "REFERENCE", obAccount.Note );

        return map;
    }
}

The DomainObjectAssembler class defines generic operations and return types that circumvent compile-time checking. Obviously this could potentially cause run-time errors due to incorrect types of identity objects or factory implementations being passed in.  The DepositAccountAccessor is a wrapper that manages a DomainObjectAssembler instance and its concrete factory implementations:

public class DepositAccountAccessor
{
    // Data members
   
private Assembler assembler = null;

    // Constructors
   
public DepositAccountAccessor()
    {
        ISelectionFactory    sf  = new DepositAccountSelectionFactory();
        IUpdateFactory       up  = new DepositAccountUpdateFactory();
        IDomainObjectFactory dof = new DepositAccountFactory();

        // Now construct the deposit account assembler
       
string strConn = "...";
        string strTable = "...";
        assembler = new Assembler( strConn, strTable, sf, up, dof );
    }

    public DepositAccount Read( string strEmpID, string strAcctID )
    {
        // Create an identity object to identify the employee
        DepositAccountKey key = new DepositAccountKey( strEmpID, strAcctID );

        // Get all DepositAccount objects that match 
        ArrayList alDepositAccounts = assembler.Read( key );

        // Return the first matching object
        DepositAccount ob = (alDepositAccounts.Count == 0 )? null : (DepositAccount)alDepositAccounts[0];
        return ob;
    }

    public void Write( DepositAccount ob )
    {
        // Create an identity object to identify the employee
        DepositAccountKey key = new DepositAccountKey( ob.EmployeeID, ob.AccountID );

        assembler.Write( key );
    }

    public void Delete( DepositAccount ob )
    {
        // Create an identity object to identify the employee
        DepositAccountKey key = new DepositAccountKey( ob.EmployeeID, ob.AccountID );

        assembler.Delete( ob );
    }
}

This code block is the client code that uses a DepositAccountAccessor:

 // Get a domain object from database data
DepositAccountAccessor obAccessor = new DepositAccountAccessor();
DepositAccount obAccount = obAccessor.Read( "A1", "B1");

// Modify the deposit account domain object then write it back to the database
obAccount.Note = "Account is frozen";
obAccessor.Write( obAccount );

Applicability

Use this pattern when:

Strategies / Variants

The DomainObjectAssembler contains the majority of the infrastructure code such as database resourced management, SQL statement generation, and default data translation. It relies on factory implementations for any specific customizable details. You do not need to create a subclass of the DomainObjectAssembler class to map a specific domain object, instead you define implementation of its three factory interfaces ISelectionFactory, IUpdateFactory and IDomainObjectFactory and then provide these factory implementations as constructor parameters into a new DomainObjectAssembler.

It is common for applications to read data frequently without ever having to write it. This is especially true for configuration and system control tables that are only updated by special console applications. To accommodate read-only data, you can define DomainObjectAssembler to allow the IUpdateFactory implementation to be optional - If a client passes a null for the IUpdateFactory parameter in the DomainObjectAssembler constructor, then the DomainObjectAssembler disables its write and delete operations.

The generic nature of the DomainObjectAssembler class may circumvent to some extent compile-time checking. For example, a client may initialize a DomainObjectAssembler with an inconsistent set of factory implementations, clients may also pass identity or domain object types that are different from what factory implementations expect. A straightforward solution is to create simple wrapper classes that manage single DomainObjectAssembler instances along with a valid combination of concrete factory implementations. These wrappers would encapsulate all DomainObjectAssembler initialization complexities, enforce type checking, and handle casting for specific identity objects and domain object types.

Benefits

Related Patterns