views:

337

answers:

9

I'm reading Robert Martin's book "Clean Code" and most of what I've read makes sense and I'm trying to apply as much as I possibly can. One of the simplest most basic things he talks about is that methods should be small, he goes all the way as to say that they should not contain more than three lines.

Considering that I broke down a recursive method into two smaller ones but in a code review one of my peers suggested that it looked weird and that recursion was kind of hidden.

So, what is your opinion on that? I'm including both approaches below.

This is the one method approach.

private object GetObject(string propertyName, object dataObject)
{
  object returnObject;

  if (dataObject == null) return null;

  if (propertyName.Contains("."))
  {
    Type dataObjectType = dataObject.GetType();
    PropertyInfo dataObjectProperty = dataObjectType.GetProperty(propertyName.Substring(0, propertyName.IndexOf(".")));

    ThrowExceptionIfObjectOrPropertyInexistant(dataObjectProperty, dataObjectType.FullName, propertyName);

    object nestedObject = dataObjectProperty.GetValue(dataObject, null);
    string nestedObjectPropertyName = propertyName.Substring(propertyName.IndexOf(".") + 1);

    returnObject = GetObject(nestedObjectPropertyName, nestedObject);
  }
  else
  {
    returnObject = dataObject;
  }

  return returnObject;
}

This is the two methods one.

private object GetObject(string propertyName, object dataObject)
{
  object returnObject;

  if (dataObject == null) return null;

  if (propertyName.Contains("."))
  {
    returnObject = GetNestedObject(propertyName, dataObject);
  }
  else
  {
    returnObject = dataObject;
  }

  return returnObject;
}

private object GetNestedObject(string propertyName, object dataObject)
{
  object returnObject;
  Type dataObjectType = dataObject.GetType();
  PropertyInfo dataObjectProperty = dataObjectType.GetProperty(propertyName.Substring(0, propertyName.IndexOf(".")));

  ThrowExceptionIfObjectOrPropertyInexistant(dataObjectProperty, dataObjectType.FullName, propertyName);

  object nestedObject = dataObjectProperty.GetValue(dataObject, null);
  string nestedObjectPropertyName = propertyName.Substring(propertyName.IndexOf(".") + 1);

  returnObject = GetObject(nestedObjectPropertyName, nestedObject);
  return returnObject;
}
A: 

Since somebody gave me a minus one I guess I need to elaborate. Here's what I meant:

Most people here seem to be of opinion that in this case one big function is better then several small ones. I disagree. I think your original function is a monster that has at least 3 responsibilities.

First, you should turn recursion into a loop which is always a good thing. Second, you should apply the single responsibility principle and refactor your code as follows:

    private object GetInnermostObject(object dataObject, string propertyName){
     object currentObject = dataObject;
     foreach(propName in propertyName.Split(".")) currentObject = GetNestedObject(currentObject, propName);
     return currentObject;
    }

    private object GetNestedObject(object dataObject, string propertyName)
    {
     PropertyInfo property = GetDataObjectProperty(dataObject.GetType(), propertyName);
     object nestedObject = property.GetValue(dataObject, null);
     return new DataObjectWrapper(nestedObject);
    }

    private static PropertyInfo GetDataObjectProperty(Type dataObjectType, string propertyName){
     PropertyInfo dataObjectProperty = dataObjectType.GetProperty(propertyName);
     ThrowExceptionIfObjectOrPropertyInexistant(dataObjectProperty, dataObjectType.FullName, propertyName);
     return dataObjectProperty;
    }

You can improve this further (like I suggested originally) by making this code into a separate class that would encapsulate access to dataObject altogether, but this is beyond the topic of this question.

zvolkov
Thank you for your reply. It actually works quite nicely. For this particular implementation though we decided not to encapsulate all in a separate class. But the solution is quite nice. Thanks again.
Sergio Romero
Even without a separate class you can still do the functional decomposition the way I suggested. Just make GetNestedObject return the object itself instead of the wrapper. You can see how my solution is much closer to the "3 lines per method" spirit than any other proposed.
zvolkov
+1  A: 

Your methods should be as short as possible but no shorter. In this case, all the inner function is doing is hiding the recursion. This is, as your collegue mentioned, unclear, and even misleading, since it means that the recursion proceeds

1 GetObject
2 GetNested Object
3 GetObject
4 GetNestedObject
....

It also means that the second version of the code will defeat pretty much any attempt by the compiler optimize the recursion into a loop.

Your first method fits into one screen; I think that's short enough, and the other choice is a bit confusing.

Charlie Martin
+1 co-recursion has its place, but not here
Gabe Moothart
+1  A: 

no more than 3 lines? isn't that pushing it a bit? i would say write your code in the way that makes the most sense to you and to anyone who will be maintaining your code in the future. if that means writing four lines or more, then so be it.

Jason
+1  A: 

Robert Martin has some extremly good points taken to a ridiculous extreme.

Overly long functions can be hard to both read and maintain, but advocating that methods are no longer than three lines produces code that can be cryptic and hard to follow. This is what your coworker was saying.

Take 'rules' like this as a guide and try to apply them so that the original intent (clear, concise code) is not lost.

There is nothing really wrong with your one-method code, but you may want to make it clearer by returning the simplest cases first. E.g.

if (!propertyName.Contains("."))
  return dataObject; // this is the one!

// check nested stuff
Brian
+1  A: 

I have learned to listen to your peers during a code review. If you cannot explain your code to them, then they certainly won't be able to maintain it next year.

This rule applies even more when your colleague is a psychopath who knows where you live. ;o)

Florian
A: 

too many functions means too many function names. after a certain threshold, it's very hard to tell what a function does when reading the call. you're forced to read each function's doc, adding a lot to what you have to read.

i'd say that a function has to done one thing and one only, but no less than one.

Javier
+5  A: 

How about the 1-method version with a clear guard-clause at the top, and with the recursion clearly visible at the tail end of the method:

private object GetObject(string propertyName, object dataObject)
{
  int index = propertyName.IndexOf(".");
  if (dataObject == null || index == -1)
  {
    return dataObject;
  }

  Type dataObjectType = dataObject.GetType();
  PropertyInfo dataObjectProperty = dataObjectType.GetProperty(propertyName.Substring(0, index));

  ThrowExceptionIfObjectOrPropertyInexistant(dataObjectProperty, dataObjectType.FullName, propertyName);

  return GetObject(propertyName.Substring(propertyName.IndexOf(".") + 1),
                   dataObjectProperty.GetValue(dataObject, null));
}

Creating variables for a single use sometimes just creates more semantic "noise" that a person has to parse when trying to understand code. I "collapsed" a couple of one-use variables which I believe clarifies what the recursion is doing. YMMV. I also computed the index up front so you only have to scan the string a single time to see if it contains a '.' character, and not twice.

I find the version above more readable than either of your two choices, with the bonus as others point out that with the recursion entirely contained in one method, it's easier for the compiler to unroll the recursion and turn it into a loop.

Eddie
The other proposed solutions worked as well, but this is the one I liked the most for the problem at hand. Thank you.
Sergio Romero
You're still calling `propertyName.IndexOf(".")` twice here (in the `return` line). That may be unnecessary.
strager
Also, why on earth would you turn recursion into iteration? ;P
strager
@strager: You're right. I got rid of one unnecessary call to propertyname.IndexOf(".") but I overlooked one more. And: Why turn recursion into iteration? The *compiler* does that for efficiency. Look up Tail Recursion: http://en.wikipedia.org/wiki/Tail_recursion
Eddie
@Eddie, I know the *compiler* does it. I'm wondering why *you* would do it when the compiler already does it (when the problem is naturally recursive).
strager
@strager: I didn't turn recursion into iteration above and I didn't suggest doing so. The code in my answer above is clearly recursive, not iterative. Thus, I'm no longer sure what motivates your question. I was always referring to the *compiler* doing this transformation.
Eddie
+1  A: 

Your peer has a point that the recursion is hidden. It's harder to follow two methods that are mutually recursive than one recursive method.

(Another issue in your methods is that you are mixing direct returns with a single exit point. If some of the code is working towards a single exit point, there should truly be a single exit point in the method.)

The method actually doesn't have to be recursive at all. As the recursion is the last thing that happens before the exit, it's just used to make a loop. You can easily make it iterative.

I used Split to get the strings before and after the first period, and I shortened some names. There is no reason to have very long descriptive names for local variables.

private object GetObject(string propertyName, object dataObject) {
   while (dataObject != null && propertyName.Contains(".")) {
      Type objectType = dataObject.GetType();
      string[] names = propertyName.Split('.', 2);
      PropertyInfo property = objectType.GetProperty(names[0]);

      ThrowExceptionIfObjectOrPropertyInexistant(property, objectType.FullName, propertyName);

      dataObject = property.GetValue(dataObject, null);
      propertyName = names[1];
   }
   return dataObject;
}
Guffa
This gets closer to my solution but still misses the point of applying the single responsibility principle which is what the 3 lines per method thing is all about.
zvolkov
You voted it down because I didn't write the code the same way that you did? Was it too straight forward and created too few extra objects? ;)
Guffa
A: 
Gavin Miller