On Readability and Maintainability

In the last 6 weeks, I have been spending a lot of my time rearchitecting my software. Along with this comes a lot of refactoring. It gets me to thinking about readability and maintainability. I came across a situation where there were several methods with the same boilerplate code. I reduced this down to a single method that takes a delegate and each dependant method calls into the single method with an implementation of a filter. When it was all said and done, I had sacrificed readability for maintainability. Following is a contrived example that shows my dilema:

Before:

   1: public ICollection<string> GetDataWithABC(IDataSource dataSource)
   2: {
   3:     List<string> result = new List<string>();
   4:  
   5:     using(IDataReader reader = dataSource.GetData("ABC"))
   6:     {
   7:         while(reader.Read())
   8:         {
   9:             string value = reader.GetString(0);
  10:             if(CheckAboutABC(value) && value.Contains("JUNK"))
  11:                 result.Add(value);
  12:         }
  13:     }
  14:  
  15:     return result;
  16: }
  17:  
  18: public ICollection<string> GetDataWithDEF(IDataSource dataSource)
  19: {
  20:     List<string> result = new List<string>();
  21:  
  22:     using (IDataReader reader = dataSource.GetData("DEF"))
  23:     {
  24:         while (reader.Read())
  25:         {
  26:             string value = reader.GetString(0);
  27:             if (value.Contains("BLAH"))
  28:                 result.Add(value);
  29:         }
  30:     }
  31:  
  32:     return result;
  33: }
  34:  
  35: public ICollection<string> GetDataWithXYZ(IDataSource dataSource)
  36: {
  37:     List<string> result = new List<string>();
  38:  
  39:     using (IDataReader reader = dataSource.GetData("XYZ"))
  40:     {
  41:         while (reader.Read())
  42:         {
  43:             string value = reader.GetString(0);
  44:             if (CheckAboutXYZ(value) && CheckSomethingElseXYZ(value))
  45:                 result.Add(value);
  46:         }
  47:     }
  48:  
  49:     return result;
  50: }

Notice how each of these methods has the same boilerplate code. They are already pretty straight-forward:

1. Create a list to return

2. Get the data reader from the data source

3. While there is data, Get the data, filter it and add the values to the list

4. Return the list

I think this code is pretty readable. Of course, it is not maintainable. If IDataSource is changed, for example, to return an IEnumerable, then we have to change all three methods. In my example, there were more like 10 methods with the same boilerplate code.

The only real difference in any of these methods is the filter. Why not extract the boilerplate code into a single method and pass a delegate to do the filtering? The result looks like this:

After:

   1: private delegate bool DataFilter(string value);
   2: private static ICollection<string> GetDataWithFilter(IDataSource dataSource, DataFilter filter)
   3: {
   4:     List result<string> = new List<string>();
   5:     using (IDataReader reader = dataSource.GetData("ABC"))
   6:     {
   7:         while (reader.Read())
   8:         {
   9:             string value = reader.GetString(0);
  10:  
  11:             if (filter(value))
  12:                 result.Add(value);
  13:         }
  14:     }
  15:  
  16:     return result;
  17: }
  18:  
  19: public ICollection<string> GetDataWithABC(IDataSource dataSource)
  20: {
  21:     return GetDataWithFilter(dataSource, delegate(string value)
  22:     {
  23:         return CheckAboutABC(value) && value.Contains("JUNK");
  24:     });
  25: }
  26:  
  27: public ICollection<string> GetDataWithDEF(IDataSource dataSource)
  28: {
  29:     return GetDataWithFilter(dataSource, delegate(string value)
  30:     {
  31:         return value.Contains("BLAH");
  32:     });
  33: }
  34:  
  35: public ICollection<string> GetDataWithXYZ(IDataSource dataSource)
  36: {
  37:     return GetDataWithFilter(dataSource, delegate(string value)
  38:     {
  39:         return CheckAboutXYZ(value) && CheckSomethingElseXYZ(value);
  40:     });
  41: }

I love this approach. There is no duplication of code anymore. If something changes with the boilerplate code, it can be changed in one spot. I have saved lines of code and everything is more compact.

Unfortunately, I think that the code has become less readable. First, it becomes difficult to discern what is going on there at first glance. The use of the inline delegate is a bit confusing and can trip up people who read this for the first time. Second, the double return is confusing. The inner return returns a bool where the method returns an ICollection. I have seen this trip up experienced developers who glance at the method. It is also more difficult to debug.

I could always strip the delegates out into separate methods, but I believe that is even more unreadable. Whats worse, is that we didn’t really save many lines of code. Only 9 lines of code were saved. (more were saved in my real-world scenario)

So what is better? Readability or Maintainability?

A colleague whom I have a lot of respect for told me "Always go with readable". I guess, in this case, I have to disagree with him and go with Maintainability. Duplication of code, for me, is a real big no-no. Duplication 10 times is an even bigger no-no. In this case, I think the maintainability gain outweighs the readability loss. I will even argue that as you shorten the length of each individual method, the aggregated file becomes more readable as a whole.

As a side note, it turns out that in C# 3.0, you can use lambda functions to make it a bit more readable. Unfortunately, we aren’t using 3.0 yet:

   1: public ICollection<string> GetDataWithABC(IDataSource dataSource)
   2: {
   3:     return GetDataWithFilter(dataSource, value => CheckAboutABC(value) && value.Contains("JUNK"));
   4: }

 

Leave a Reply