Optional parameter overloads in C# and cascading calls...
This is a performance versus design considerations post. You an weigh in either way and your input will be valuable.
Optional Parameter Overloads
C# implements optional parameters through overloads. Sometimes this imposes combinatorial design considerations, while other times it creates a deep stack. In either case, everyone agrees that many methods simply have default parameters and it is much easier to call them with their defaults than to figure out what the values should be. Let's take a simple example on the ArrayList in BinarySearch.
BinarySearch requires at the terminal method, start/end offsets, a value to search for, and a comparison routine used to determine relative equality. With those parameters you can affect every behavior of the method by changing one thing or the other, but a BinarySearch also has a default set of logic that you might assume, in which only the value being searched for matters. The start/end offsets are completely useless if you want to search the ENTIRE collection and the comparison routine isn't important unless you want explicit control over how the values are going to be examined. This leads to a couple of immediate overloads:
// Actual ArrayList overloads
BinarySearch(object value);
BinarySearch(object value, IComparer comparer);
BinarySearch(int start, int length, object value, IComparer comparer);
// Additional possibilities
BinarySearch(int start, int length, object value);
BinarySearch(int start, object value);
BinarySearch(int start, object value, IComparer comparer);
The additional possibilities are where the optional parameters start to fail. Since we have two integer parameters, users looking at the method description can't really understand what value the integer should take. You are also removing the concept of a range from the binary search by searching from some starting offset all the way to the end. This is a common routine in other methods, such as Substring, IndexOf, etc..., but not quite as common in BinarySearch. Managing all of those overloads is another problem altogether. After all, where do the various defaults come from?
Using Cascading Calls
A cascading call is a call routine where you start with the most basic method signature, find the closest matching signature with more parameters, and add defaults. That method may in turn do the same thing and so your methods cascade. In this way if you change the defaults in your more specific method, it updates the defaults for all of the less specific methods as well. Let's see this in action.
public int Foo(string bar) { return Foo(bar, defaultBaz); }
public int Foo(string bar, string baz) { return Foo(bar, baz, defaultBam); }
public int Foo(string bar, string baz, string bam) { return 1; }
Changing defaultBam has an effect on both itself and all methods that rely on it to fill n extra default parameters. This is a common API design allowing for changes to propagate the entire overload structure that reduces your focus to a method scope locality. Most likely you can safely work on the second overload to implement some performance optimization, change the way defaultBam is computed, or change variable names without ever touching the first overload. Once the changes are made everything just works and the less specific overloads all benefit from the changes that you made. This is definitely true of terminal methods in the call chain. Terminal methods are where most of the behavior resides. In our case our behavior is to return 1, but changing that to 2, instantly impacts and modifies the behavior of all our other methods.
Using Defaulted Constants
Often times you can replace cascading calls with defaulted constants. As you perform this operation, much of your code locality begins to break down unless you adhere to some rigid guidelines. Even then, changes to the terminal method signature mean much pain, since you'll now have to change all of the other methods to match the new signature. Defaulted constants in our sample would appear as:
// All overloads call the Terminal Method directly
public int Foo(string bar) { return Foo(bar, defaultBaz, defaultBam); }
public int Foo(string bar, string baz) { return Foo(bar, baz, defaultBam); }
public int Foo(string bar, string baz, string bam) { return 1; }
// Changing the constant name means changing in two places
public int Foo(string bar) { return Foo(bar, defaultBaz, defaultBammo); }
public int Foo(string bar, string baz) { return Foo(bar, baz, defaultBammo); }
// Changing the terminal means opening a can of beans
public int Foo(string bar) { return Foo(bar, defaultBaz, defaultBam, defaultBark); }
public int Foo(string bar, string baz) { return Foo(bar, baz, defaultBam, defaultBark); }
public int Foo(string bar, string baz, string bam) { return Foo(bar, baz, bam, defaultBark); }
public int Foo(string bar, string baz, string bam, string bark) { return 1; }
We obviously get more performance and a smaller stack out of the direct call methods, but changing the name of that constant meant hitting two methods. Well it is covered by refactoring right? Not much of a hit at all if the tool does the work for us. but what about changing the terminal. That meant not only adding a new method, but updating all three existing methods. That won't be covered by refactoring and it'll be left up to you.
The Decision!
Cascading calls in APIs are definitely a great tool, especially if there are a large number of parameters, perhaps 5-10 or even more. They introduce a level of flexibility that allows you to change the terminal, add or remove terminal functionality, and provide method locality for changes to default parameters. On the flip side they hurt your performance a bit, enough that you'd want to refactor them out of often used methods. They increase your stack frame size, so calling them from recursive functions might be a bit dangerous (especially if your recursive functions are designed to approach your stack ceiling). The BCL is a mixture of both cascading calls and direct calls depending on the APIs so there must be a design criterion that changes when you use one over the other? Or maybe it is just a personal developer decision?