1

I have noticed that I am repeating the same few lines of code over and over and I want to refactor to prevent that for upkeep. However, the changes between each iteration is in the middle of all the validation, so I'm not sure the best way to accomplish this. I feel like there is some way to write a generic function that can accept an input to change the inner code, but the only way I can think of is an enumeration and a switch statement for each one.

Are there any better methods for making this block of repeated code more readable and maintainable?

Specific block repeated throughout:

    if (_read.HasRows)
    {
        while (_read.Read())
        {
            //DO SOMETHING...;
        }
    }
    _read.Close();

Example usages:

public List<object> TableList()
{
    List<object> list = new List<object>();

    _cmd.CommandText = "SELECT * FROM sys.tables";

    try
    {
        _read = _cmd.ExecuteReader();
    }
    catch (Exception e)
    {
                     //please ignore this try catch for now
        list[0] = e; //this is a temporary measure until I finalize error handling
    }

//block to generalize
    if (_read.HasRows)
    {
        while (_read.Read())
        {
            list.Add(_read.GetValue(0)); //unique line
        }
    }


    _read.Close();

    return list;
}

another example:

private List<object[]> _Columns = new List<object[]>()    
private bool SetColumns()
{
    _oldCommand = _cmd.CommandText;
    _cmd.CommandText = "exec sp_columns " + tableName;
        try
        {
            _read = _cmd.ExecuteReader();
        }
        catch (Exception e)
        { }  //again, ignore the messy error handling.  That is coming next.


//block to generalize
    if (_read.HasRows)
    {
        while (_read.Read())
        {
            _Columns.Add(new object[2] { _read.GetValue(3), _read.GetValue(5) }); //unique line
        }
    }



    _read.Close();
    _cmd.CommandText = _oldCommand;
    if (_Columns.Count != 0)
    {
        return true;
    }
    else
    {
        return false;
    }
}

Just a reminder at the bottom of the specific scope of the question. How can I generalize the repeated block of code while still allowing the unique lines to exist per different use? I only gave two examples, but there are several instances of this issue throughout and I would like to refactor before going to deep.

Final Implementation after seeing answers:

    /// <summary>
    /// Sets a list of tables in the connected database.
    /// </summary>
    /// <returns>Returns a List<string> of the tables.  If there is an error, then the first element of the list will conatin the exception.</returns>
    public List<string> TableList()
    {
        List<string> list = new List<string>();

        _cmd.CommandText = "SELECT * FROM sys.tables";

        ExecuteReader(_read => { list.Add(_read.GetString(0)); });

        _read.Close();

        return list;
    }


    /// <summary>
    /// sets column names and types to the _columns private variable [0] = name, [1] = type
    /// </summary>
    private void SetColumns()
    {
        _oldCommand = _cmd.CommandText;

        _cmd.CommandText = "exec sp_columns " + TableName;

        ExecuteReader(_read => columns.Add(new string[2] { _read.GetString(3), _read.GetString(5) }));

        _cmd.CommandText = _oldCommand;

        _read.Close();

    }


    /// <summary>
    /// Executes the reader with error handling and type returns.
    /// </summary>
    /// <param name="readline">the code that actually pulls the data from each line of the reader</param>
    private void ExecuteReader(Action<SqlDataReader> readline)
    {
        if (_con.State.HasFlag(ConnectionState.Closed)) { _con.Open(); }

        _read = _cmd.ExecuteReader();

        while (_read.Read())
        {
            readline(_read);
        }

        _con.Close();

    }

I chose this method as it felt the most readable with my level of experience. I have done some looking into Dapper as many others have recommended. I'll most likely be using it in future projects as I like to write my own boilerplate code at least once for my own experience sake.

Thank you everyone for the amazing answers. I hope others find this helpful in the future.

Mikey Awbrey
  • 94
  • 2
  • 8
  • You might want to take a look at [Dapper](https://github.com/StackExchange/dapper-dot-net) as it takes care of alot of the boilerplate code associated with ADO.Net. – juharr Jan 16 '17 at 15:17
  • 1
    [You don't need to check `HasRows`](http://stackoverflow.com/questions/14196644/should-if-call-sqldatareader-hasrows-if-i-am-calling-sqlreader-read) – stuartd Jan 16 '17 at 15:19
  • Do you want to want to unify example 1 with example 2, or that you wan to generalize each one of them? – Ghasan غسان Jan 16 '17 at 15:22
  • Thanks for the comment @stuartd I usually go a bit crazy with validations and don't take the time to see if they are necessary. Changes like that are exactly why I am trying to generalize so I can make those kinds of changes without having to change 100 things. – Mikey Awbrey Jan 16 '17 at 15:36
  • @Ghasan I want to unify those examples. The `if (read_.HasRows)` block, and other very similar examples, are repeating too many times. After the third copy paste with a single line change, I knew I was doing something wrong. The lamba expression answers have been right on point so far. – Mikey Awbrey Jan 16 '17 at 15:38

5 Answers5

1

You can factor out the population code by passing in a lambda expression, for example:

SetColumns(reader => _Columns.Add(new object[2] { _read.GetValue(3), _read.GetValue(5) }));

SetColumns(reader => list.Add(reader.GetValue(0)));

See the updated line inside the while loop:

private List<object[]> _Columns = new List<object[]>();
private bool SetColumns(Action<DbDataReader> populateColumns)
{
    _oldCommand = _cmd.CommandText;
    _cmd.CommandText = "exec sp_columns " + tableName;
        try
        {
            _read = _cmd.ExecuteReader();
        }
        catch (Exception e)
        { }  //again, ignore the messy error handling.  That is coming next.


//block to generalize
    if (_read.HasRows)
    {
        while (_read.Read())
        {
            populateColumns(_read);
        }
    }



    _read.Close();
    _cmd.CommandText = _oldCommand;
    if (_Columns.Count != 0)
    {
        return true;
    }
    else
    {
        return false;
    }
}
Trey Tomes
  • 76
  • 9
  • I don't fully understand the `populateColumns(_read)` expression. Does the lamba expression automatically populate that object? What does the lamba expression return, a `DbDataReader` or `object[2]`? – Mikey Awbrey Jan 16 '17 at 15:28
  • In this case, the lambda expression doesn't return anything (because it is an Action), and takes a DbDataReader as a parameter. When you execute `populateColumns(_read)`. The lambda expression you pass in becomes an anonymous function, assigned to the parameter `populateColumns`. Each time you call `populateColumns` in your while-loop, it will execute that function with `_read` as the parameter value. There is a pretty thorough explanation of how to use lambda expressions on [MSDN](https://msdn.microsoft.com/en-us/library/bb397687.aspx). – Trey Tomes Jan 16 '17 at 15:36
  • I'll be sure to read up on them again. It must not have clicked the first time, but seeing everyone's answers and comments have really made it more clear. I spent to much time with proprietary scripting languages. Learning a more robust, lower level language has been slower than I'd like. – Mikey Awbrey Jan 16 '17 at 15:41
1

There comes 2 ways out into the mind when you are thinking about the reuse.

  • Just make it a method, to use from everywhere.
  • Inheritance

Now in this case inheritance sounds a little pushy. But making a method can go right.

Here is my shot:

public class FillerFromDb
{
    public IList ListToBeFilled { get; set; }
    SqlCommand Command { get; set; }
    SqlDataReader Reader { get; set; }
    public FillerFromDb ( IList listToBeFilled, SqlCommand commandToRun )
    {
        ListToBeFilled = listToBeFilled;
        Command = commandToRun;
    }

    public void RunFor ( Func<SqlDataReader, object> rowProcessing )
    {
        ReadFromDb();
        ProcessRow(rowProcessing);
    }

    private void ReadFromDb ()
    {
        try
        {
            Reader = Command.ExecuteReader();
        }
        catch ( Exception e )
        {
            // handle the exception
        }
    }

    private void ProcessRow ( Func<SqlDataReader, object> rowProcessing )
    {
        if ( Reader.HasRows )
        {
            while ( Reader.Read() )
            {
                var result = rowProcessing(Reader);
                ListToBeFilled.Add(result);
            }
        }

        Reader.Close();
    }
}

And the callee:

var filler = new FillerFromDb(list/*or _Columns*/, _cmd);

filler.RunFor(row => row.GetValue(0));

If you are into refactoring, wrapping your worker functions in classes like this, you would feel better.

Tolga Evcimen
  • 7,112
  • 11
  • 58
  • 91
  • Just looking for a bit of clarification. is `ProcessColumns` in your method different from `processcolumns` on purpose? I am relatively unclear on variable / type handling with these lambda expressions. – Mikey Awbrey Jan 16 '17 at 15:34
  • @MikeyAwbrey looks like a typo to me, it should match the parameter name. The idea here is to pass in the 'different bit' as a function to be executed at the relevant point. – Charles Mager Jan 16 '17 at 15:35
0

Try this:

private void CommonBlock(Action act)
    {
        // some code
        if (act!=null) act.Invoke();
        //next code
    }

to call with the specific code use:

CommonBlock(()=>{Debug.WriteLine("you action");/*Anything you want to do*/});
Leonid Malyshev
  • 475
  • 1
  • 6
  • 14
  • This is pretty clear, but will the `act.Invoke()` work exactly as written, or will I need to give it more information (types, variable names, etc)? If I had a variable declared within the scope of the `some code`, could I pass that into the `Action` object from another method? – Mikey Awbrey Jan 16 '17 at 15:30
  • If some code (var etc) is available in you old code then it's available in the CommonBlock(). So you can pass them to the action. And you don't need any more info. Just change debug.writeline to your code (eg list.Add(_read.GetValue(0)); ) – Leonid Malyshev Jan 16 '17 at 15:40
0

Simplest way is to create an extension method:

public static class Extensions
{
  public static IDataReader Read(this IDataReader reader, Action<IDataReader> action)
  {
    if(reader.HasRows)
    {
      while(reader.Read())
      {
        action(reader);
      }
    }
  }
}

An example, using your original sample, would be:

public List<object> TableList()
{
    List<object> list = new List<object>();

    _cmd.CommandText = "SELECT * FROM sys.tables";

    try
    {
        _read = _cmd.ExecuteReader();
    }
    catch (Exception e)
    {
                     //please ignore this try catch for now
        list[0] = e; //this is a temporary measure until I finalize error handling
    }

    _read.Read(r => list.Add(r.GetValue(0)));        
    _read.Close();

    return list;
}
Eric
  • 1,737
  • 1
  • 13
  • 17
  • I like the method of implementing this as an extension. Looks very clean. This could generate some confusion down the line though if I'm not paying attention. I don't think my API knowledge is deep enough to recognize that as an extension first glance 6 months from now (unless I very obviously noticed the extension class). – Mikey Awbrey Jan 16 '17 at 16:44
  • The extension method is the most benign way of doing this. If you are looking down the line, I would look towards Dapper and its associated projects (e.g. Extensions/Extensions.Linq) – Eric Jan 16 '17 at 17:12
  • @juharr mentioned Dapper earlier. I've taken a brief look at it. I do believe in the future I will use more practical applications like that, but I find working through boilerplate stuff once myself helps me really understand what I'm doing better. Thank you for the recommendation and answer. – Mikey Awbrey Jan 16 '17 at 18:26
0

Pass the differences into the function via an Action or Func. Write one common version and then just pass in what is different to be executed.

Kelly
  • 6,992
  • 12
  • 59
  • 76
  • Others have well covered the method of using an action, but could you clarify on the usage of Func in the context of this example? I was reading the [MSDN](https://msdn.microsoft.com/en-us/library/bb549151(v=vs.110).aspx) page on them and I'm not sure how to practically apply it to this. – Mikey Awbrey Jan 17 '17 at 14:49