Resource decorator

Summary

Purpose

A resource decorator dynamically attaches behavior to an existing resource with minimal disruption to application code. A resource decorator extends a resource's functionality without sub-classing or changing functionality.

Scenario

A database drive is a middleware entity that manages interactions between an application and its physical database.  Database drivers like ODBC make is possible for applications to avoid hardwiring references to a specific database platform. Applications invoke generic interfaces declared by these drivers that in turn implement them for a specific database product.

Suppose that you want to extend the behavior of an existing database driver by making it support customized logging. You can enable this logging by dynamically attaching customized logging functionality. However, database drives are often commercial products with no available source code. Further, extending the drive by subclassing is not a feasible idea as you will need to provide a similar subset of classes for every database drive you support.

The Resource Decorator, which is an instance of the Decorator pattern, applies directly to database resources. A resource decorator allows you to attach additional behavior to an existing database driver without subclassing and with minimal disruption to code. The key concept in the resource decorator pattern is that a decorator object implements the same interface as the decorated object. The application invokes the decorator's interface as if it was invoking the original decorated object. The decorator may perform some pre-processing before dispatching these operations to the decorated object, or it may perform some post-processing after dispatching these operations to the decorated object, or the processing may occur before and after dispatching these operations to the decorated object.

Resource decorator's are primarily useful as debugging and logging aids, but they can also provide other functions such as tweaking SQL statements, enforcing application-level security, preventing write operations, caching result sets and so on.

Structure & UML

The static structure is shown below:

The Resource interface represents any database resource such as a database connection while ConcreteResource implements this interface in conjunction with a database platform. For example, ADO.NET IDbConnection represents a Resource nterface while SqlConnection represents a ConcreteResource for SQL Server database. Client code is normally written in terms of IDbConnection but interacts directly with a SqlConnection object. BaseResourceDecorator is an abstract class that maintains a reference to the Resource implementation (SqlConnection) that is meant to be decorated with extra functionality. BaseResourceDecorator delegates every operation to its referenced resource. ConcreteResourceDecorator inherits BaseResourceDecorator automatic operation delegation but adds its own custom behavior. ConcreteResourceDecorator only contains code related to its additional behavior while all delegation logic is handled by the BaseResourceDecorator. The client code continue to use the Resource interface, but you can plug in one or more ConcreteResourceDecorator that wrap a ConcreteResource of your choice.

The sequence diagram below shows what happens when a client invokes OperationA on a concrete resource decorator. In this sequence, ConcreteResourceDecorator invokes its custom behavior before delegating OperationA to its assigned ConcreteResource.:

Example

The following code shows how a base and concrete decorators can be used to decorate the SqlConnection ADO.NET object with custom logging functionality:

public class BaseConnectionDecorator : System.Data.IDbConnection
{
    // Data members
    private IDbConnection m_connection = null;

    // Constructors
    public BaseConnectionDecorator( IDbConnection conn)
    {
        m_connection = conn;
    }

    // IDbConnection implementation
 
   // Methods
    virtual public IDbTransaction BeginTransaction(IsolationLevel level) { return m_connection.BeginTransaction( level ); }
    virtual public IDbTransaction BeginTransaction()                     { return m_connection.BeginTransaction(); }
    virtual public void           Close()                                { m_connection.Close(); }
    virtual public void           ChangeDatabase( string strDBName )     { m_connection.ChangeDatabase( strDBName ); }
    virtual public IDbCommand     CreateCommand()                        { return m_connection.CreateCommand(); }
    virtual public void           Open()                                 { m_connection.Open(); }
    public void                   Dispose()                              { m_connection.Dispose(); }
    // Properties
    public string ConnectionString 
    {
        get { return m_connection.ConnectionString; }
        set { m_connection.ConnectionString = (string)value; }
    }
    public int ConnectionTimeout
    {
        get { return m_connection.ConnectionTimeout; }
    }
    public string Database
    {
        get { return m_connection.Database; }
    }
    public ConnectionState State
    {
        get { return m_connection.State; }
    }
}

public class LoggingConnection : BaseConnectionDecorator
{
    // Data members
    private bool m_bClosed = true;
    private ConnectionTimer m_Timer = null;

    // Constructors
    public TimedConnection( IDbConnection conn) : base(conn)
    {
    }


    // All IDbConnection overridden implementations need to output tracing statements
   public override void Open()
    { 
        XTrace.EnterFunction();
        base.Open ();
        XTrace.ExitFunction();
    }
    public override void Close()
    {
        XTrace.EnterFunction();
        base.Close ();
        XTrace.ExitFunction();

    }
    public override IDbTransaction BeginTransaction(IsolationLevel level)
    {
        try
        {
            XTrace.EnterFunction();
            return base.BeginTransaction( level ); 
        }
        finally { XTrace.ExitFunction(); }
    }
    public override IDbTransaction BeginTransaction()
    {
        try
        {
            XTrace.EnterFunction();
            return base.BeginTransaction(); 
        }
        finally { XTrace.ExitFunction(); }
    }
    public override void ChangeDatabase( string strDBName )
    { 
        XTrace.EnterFunction();
        base.ChangeDatabase( strDBName ); 
        XTrace.ExitFunction();
    }
    public override IDbCommand CreateCommand()
    {
        try
        {
            XTrace.EnterFunction();
            return base.CreateCommand();
        }
        finally { XTrace.ExitFunction(); }
    }
}

// Client code

// Create a connection object
LoggingConnection conn = new LoggingConnection( new SqlConneciton() );
conn.ConnectrionString = "DSN= ... ";

// Create a command object and initialize it with a LoggingConnection
SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandText = "Select * from T";
cmd.ExecuteDataReader();
...

Applicability

Use this pattern when:

Strategies / Variants

Consider these strategies when designing a resource decorator

Benefits

Liabilities

Related Patterns