Click here to Skip to main content
15,884,473 members
Articles / Programming Languages / C#

Refactoring Copy/Paste Pattern with Data-Structures

Rate me:
Please Sign up or sign in to vote.
4.50/5 (6 votes)
20 Jan 2011CPOL4 min read 25K   112   13   4
Using data-structures to do away with boiler plate code.

Introduction

Following up the "Refactoring Copy/Paste Code With the Use of Delegates", I will ramble a bit more on how to refactor copy/paste code that exists in the wild.

Background

There are many situations where code blocks get repeated over and over again; if there are only a few occurrences, that's not so bad. But, eventually there's a limit beyond more than a few, which means you should try another approach.

One of the most common occurrences of repetition is in projects where there are a lot of calls to Stored Procedures or queries, which is the case in most CRUD applications. And so, any class that has code for database access will have a lot of methods in which only a small portion of code lines differ from each other. This has a knack of making the code difficult to update, when changes have to be shared by all methods. There are countless opportunities for mistakes that arise from boredom or a botched search-and-replace edit.

The solution to this problem is that we have to separate the main flow from the data. That is, refactoring this kind of pattern by creating custom data-structures that are adapted to the needs of one's application. Also, one can add some cleverness into them, making it easier to develop the handling code.

Exemplifying with ADO.NET

We start with this common pattern; as you can see, it's mostly boiler plate code. And, if there are only a small number of occurrences, there's nothing wrong to use it this way.

C#
public DataSet GetPersonsInformationWithFilter(char filter_sex)
{
    DataSet persons = new DataSet();
    string connectionstring = 
      ConfigurationManager.ConnectionStrings["default"].ConnectionString;
    SqlConnection connection = new SqlConnection(connectionstring);
    string query = "SELECT * FROM PERSONS WHERE PERSONS.SEX = @sex ;";
    SqlCommand command = new SqlCommand(query, connection);
    SqlDataAdapter adapter = new SqlDataAdapter(command);
    SqlParameter sex_parameter = 
        command.Parameters.Add("@sex", SqlDbType.Char);

    sex_parameter.Direction = ParameterDirection.Input;
    sex_parameter.Value = filter_sex;

    try
    {
        connection.Open();
        adapter.Fill(persons);
    }
    catch
    {
        // do some logging here ...
        throw;
    }
    finally
    {
        connection.Close();
        adapter.Dispose();
        command.Dispose();
        connection.Dispose();
    }

    return persons;
}

In this case, it is relatively easy to refactor these types of methods through the use of data structures that model the database call operation. And this can be done while maintaining the method signatures so that the rest of the application code doesn't get broken.

Here are some simple data structures to model the SQL command and the SQL parameter, conveniently called QueryDefinition and ParameterDefinition.

The QueryDefinition class will be responsible for defining the database command call, be it a query (select, update, insert, delete) or a Stored Procedure call. The ParameterDefinition class will define the input/output parameters.

C#
public class QueryDefinition
{
    public string ConnectionSetting
    {
        get;
        set;
    }

    public string CallText
    {
        get;
        set;
    }
    
    public CommandType CallType
    {
        get;
        set;
    }

    public List<ParameterDefinition> Parameters
    {
        get;
        set;
    }
}

public class ParameterDefinition
{
    public ParameterDefinition(string name, SqlDbType dbType, 
           ParameterDirection direction)
    {
        this.Name = name;
        this.DbType = dbType;
        this.Direction = direction;
    }

    public ParameterDefinition(string name, SqlDbType dbType, 
           ParameterDirection direction, int size)
        : this(name, dbType, direction)
    {
        this.Size = size;
    }

    public ParameterDefinition(string name, SqlDbType dbType, 
           ParameterDirection direction, object value)
        : this(name, dbType, direction)
    {
        this.Value = value;
    }

    public ParameterDefinition(string name, SqlDbType dbType, 
           ParameterDirection direction, int size, object value)
        : this(name, dbType, direction, size)
    {
        this.Value = value;
    }

    public string Name
    {
        get;
        set;
    }

    public SqlDbType DbType
    {
        get;
        set;
    }

    public int? Size
    {
        get;
        set;
    }

    public ParameterDirection Direction
    {
        get;
        set;
    }

    public object Value
    {
        get;
        set;
    }
}

These classes store the minimum requirements for building an ADO.NET command; these will be passed as a parameter and define an operation, like getting a list of receipts or inserting customer information.

I will also add a clever feature, adding a delegated method to deliver the output. This will make the code more generic and easier to reuse for most types of output.

C#
protected T AccessDataBase<T>(QueryDefinition definitions, 
            Func<SqlCommand, T> output_functor)
{
    using (SqlConnection connection = new SqlConnection(
           ConfigurationManager.ConnectionStrings[
           definitions.ConnectionSetting].ConnectionString))
    {
        try
        {
            connection.Open();

            using (SqlCommand command = 
                   this.CreateCommand(definitions, connection))
            {
                return output_functor(command);
            }
        }
        catch (Exception ex)
        {
            throw;
        }
        finally
        {
            connection.Close();
        }
    }
}

protected IEnumerable<T> AccessDataBase<T>(
          QueryDefinition definitions, Func<SqlDataReader, T> map_functor)
{
    using (SqlConnection connection = new SqlConnection(
        ConfigurationManager.ConnectionStrings[
        definitions.ConnectionSetting].ConnectionString))
    {
        using (SqlCommand command = 
                this.CreateCommand(definitions, connection))
        {
            connection.Open();
            
            using (SqlDataReader reader = command.ExecuteReader())
            {
                while (reader.NextResult())
                {
                    yield return map_functor(reader);
                }
            }
            
            connection.Close();
        }
    }
}

private SqlCommand CreateCommand(QueryDefinition definitions, 
                                 SqlConnection connection)
{
    SqlCommand ret_command = new SqlCommand(definitions.CallText, 
               connection) { CommandType = definitions.CallType };

    foreach (SqlParameter parameter in 
             this.BuildParameters(definitions.Parameters))
        ret_command.Parameters.Add(parameter);
    
    return ret_command;
}

private IEnumerable<SqlParameter> BuildParameters(
        IEnumerable<ParameterDefinition> definitions)
{
    foreach (ParameterDefinition item in definitions)
        yield return this.BuildParameter(item);
}

private SqlParameter BuildParameter(ParameterDefinition definition)
{
    SqlParameter retParam = null;

    if (definition.Size != null)
        retParam = new SqlParameter(definition.Name, 
              definition.DbType, 
              Convert.ToInt32(definition.Size)) 
              { Direction = definition.Direction };
    else
        retParam = new SqlParameter(definition.Name, 
                     definition.DbType) 
                     { Direction = definition.Direction };

    if ((definition.Direction == ParameterDirection.Input || 
         definition.Direction == ParameterDirection.InputOutput)
         && definition.Value != null)
        retParam.Value = definition.Value;

    return retParam;
}

As you can see, there are two AccessDataBase methods, one for dealing with a single return, either object or value type, and IEnumerable for dealing with queries whose output can return more than one row.

The method that returns an enumeration is also different because its anonymous function is a mapping operation, so as it iterates through the data adapter class, it will transform the selected data onto the expected return type.

I will now add some helper methods to populate the parameters list and to setup a cleaned up body method responsible for calling the Stored Procedure or query command. These are over the counter and ready to use helper methods that do the most frequently used operations.

C#
protected DataSet AccessDbDataSet(QueryDefinition definitions)
{
    Func<SqlCommand, DataSet> out_functor = 
           (SqlCommand command) => this.GetDataSet(command);

    return this.AccessDataBase<DataSet>(definitions, out_functor);
}

protected int AccessDbNonQuery(QueryDefinition definitions)
{
    Func<SqlCommand, int> out_functor = 
           (SqlCommand command) => command.ExecuteNonQuery();

    return this.AccessDataBase<int>(definitions, out_functor);
}

In this case, I made the decision that the pattern of usage of this class would be by sub-classing, or deriving from it. But this can also be done by writing the class as a utility class.

So, here are the simplified methods; the class method signature can remain the the same, but now, changes to the inner code and structure are easier to perform.

C#
public DataSet GetSomeProcedure()
{
    QueryDefinition definition = new QueryDefinition()
    {
        ConnectionSetting = this._connection,
        CallText = "ProcedureName",
        CallType = CommandType.StoredProcedure,
        Parameters = new List<ParameterDefinition>() 
            { 
                new ParameterDefinition("result", 
                    SqlDbType.Structured, ParameterDirection.ReturnValue)
            }
    };

    return this.AccessDbDataSet(definition);
}

public DataSet GetSomeQuery(int id)
{
    QueryDefinition definition = new QueryDefinition()
    {
        ConnectionSetting = this._connection,
        CallText = "select * from atable where ID=@id;",
        CallType = CommandType.Text,
        Parameters = new List<ParameterDefinition>() 
            { 
                new ParameterDefinition("@id", 
                    SqlDbType.Int, ParameterDirection.Input) { Value = id}
            }
    };

    return this.AccessDbDataSet(definition);
}

public IEnumerable<Person> GetPersons()
{
    QueryDefinition definition = new QueryDefinition()
    {
        ConnectionSetting = this._connection,
        CallText = "select name, address, age from persons;",
        CallType = CommandType.Text,
        Parameters = new List<ParameterDefinition>() { }
    };
    Func<SqlDataReader, Person> map_functor = 
        (reader) => new Person() { 
                    Name = reader["name"].ToString(),
                    Address = reader["address"].ToString(),
                    Age = (int) reader["age"]
                };

    foreach (Person item in 
                this.AccessDataBase<Person>(definition, map_functor))
        yield return item;
}

The first consequence is that the code gets more terse and you only write what you need to, like defining input and output. So, there are less opportunities for errors. Second, you have opened new ground for more interesting changes that can simplify even more the writing of your data layer. You can write a Fluent interface to deal with the whole process; you can also, if you need to, develop a serialization strategy for your queries without writing too much new code.

Points of Interest

This pattern of refactoring is not only useful for simplifying database access code, but any situation where the input and output parameters are the only variant code sections in the class methods.

This kind of refactoring has other consequences as well, it means that we can serialize the data-structures and turn the whole process more dynamic, easing up the need for compiling new binaries when changes on the data structure don't break the code.

History

  • First submission - 06-01-2011.
  • Updated submission - 13-01-2011.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Portugal Portugal
Software Dev Gun For Hire.

Comments and Discussions

 
GeneralTitle is misleading Pin
Tomas Brennan19-Jan-11 10:04
Tomas Brennan19-Jan-11 10:04 
GeneralRe: Title is misleading Pin
Carlos Conceição19-Jan-11 10:51
Carlos Conceição19-Jan-11 10:51 
GeneralNice idea Pin
Sacha Barber19-Jan-11 6:16
Sacha Barber19-Jan-11 6:16 
GeneralRe: Nice idea Pin
Carlos Conceição19-Jan-11 10:46
Carlos Conceição19-Jan-11 10:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.