I think I've written about this before but I don't care. I'm going to keep harping on it. Error detection and handling should be on the edges of programs, not on the inside. You shouldn't have to get halfway through your execution to discover that something is wrong.
It's nice to have this sort of philosophy, but how do you put it into practice? Let's look at an example.
This morning I was working on some code repository mining code and I needed to group some events. I knew what to do, I reached for group_by in Ruby. It's a method on arrays which takes a block and executes for each element in the array. Every unique return value from the block turns into a key in a hash that group_by returns. The values in the hash are all of the elements which produced the result value when the block was run on them.
Here's a simple example:
events.group_by {|e| e.method_name }
When I execute that, I get a hash with method names as keys. Each key in the hash is associated with all of the events that have that method name.
So, where do the errors come in? Well, the method names in my system are fully qualified. "Utilities::Config::search" is an example. The name denotes a method named search on the Config class in the Utilities module. Now, suppose that I want to group on the names of classes?
It seems easy enough. Here's a Ruby function that which will let me do that:
def class_name method_name method_name.split('::')[0..-2].join('::') end events.group_by {|e| class_name(e.method_name) }
The function takes the name and splits it on '::'. Then it takes all segments except for the last and joins them up again. That should be our class name. There is a problem, though. It doesn't work for methods without a class. It returns nil because [0..-2] on an empty array is undefined.
Hmm.. What can we do? Well, my first tack was to just return "" from class_name when there was only one segment:
def class_name method_name return "" if method_name.split('::') <= 1 method_name.split('::')[0..-2].join('::') end
but then we have to get rid of that entry:
events.group_by {|e| class_name(e.method_name) }.select {|k,_| not k.empty? }
More than a bit ridiculous, eh?
Is there a better solution? Exceptions to the rescue!! No, just kidding.
Whenever you have an error, it pays to see if you can turn it into a normal case. In this case, it's very easy. It turns out that all methods in Ruby are part of a class. When you don't see a class in the source file, Object is the implicit class.
Here's the fix:
def class_name method_name return "Object" if method_name.split('::').count <= 1 method_name.split('::')[0..-2].join('::') end
Now all methods that are defined without a class are binned under Object. For instance, our class_name method will be seen as Object::class_name.
It might seem that we got lucky here, that the design of Ruby gave us an easy default. Well, the truth is more sublime. Ruby is playing the same trick that we are. In essence, Ruby is saying that methods that don't appear to have a class aren't really special. They are part of Object. Here's the nice thing. If Ruby hadn't played this trick, we could've. We could've created a name like "no_class" and used it as a bin for all methods that aren't "part of" a class.
Now that we have our class_name function, we can group away in an error-less way:
events.group_by {|e| class_name(e.method_name) }
And we can compose without checking for error cases:
events.group_by {|e| class_name(e.method_name) }.values.map(&:count).freq
because there are none.
Would you say that the issue is Primitive Obsession and that a variation of the Null Object Pattern was the solution in this case?
Posted by: Truewill | September 16, 2011 at 10:35 AM
(Please ignore the bit about Primitive Obsession - I misread the article.)
Posted by: Truewill | September 16, 2011 at 10:54 AM
Definitely Primitive Obsession here: #class_name one day wants to be a method on FullyQualifiedMethodName. Not soon, mind you, but one day.
Posted by: Jbrains | September 16, 2011 at 01:26 PM
I don't see what the problem is with just allowing the code to throw an exception and then handling the error at that time.
Posted by: William Huong | September 16, 2011 at 07:54 PM
"Finessing Away Errors" seems to be a special case of "Designing Away Constraints" (albeit an important one), doesn't it?
Posted by: Bob Lauer | September 17, 2011 at 03:34 PM
How long did it take you to write this? This is Absolutely fantastic.
Posted by: seetoo | September 22, 2011 at 07:07 AM
This is sort-of an application of the Null Object pattern. Or at least, it's the same reasoning that leads to the Null Object pattern.
Posted by: Alex Young | September 23, 2011 at 02:19 AM
@William Huong: I think the problem with raising and catching exceptions is that you don't end up with a useable result in that case, full of the non-exception cases. Doing it the way michael outlines, you always get a useable result out, so your program produces the desired output - even if there were 'error cases' in the mix.
Posted by: Jonathan Hartley | September 23, 2011 at 03:45 AM
So it is exactly what I need and I will keep it.
So. I think the best way to avoid mistake is that you do it yourself.
Posted by: iPhone contacts backup | February 09, 2012 at 11:11 PM
184 I’ve read through a number of the articles in your website , and I love the way you blog. I included it to my favorites blog site list and will also be checking quickly. http://www.cheaphatsmart.com
Posted by: cheap hats | April 13, 2012 at 10:39 PM