Constructor Overloads: Know When to Say When
Paralysis By Options
Do you ever find yourself in a situation where some API or another requires you to instantiate an object? (If you’re reading this blog, the answer is probably “yes”). What do you usually do at this point? Instantiate it, compile, and make sure you’re good before poking around to see what your new object has to offer, usually in the form of auto-complete/intellisense? I think that’s what most would do. Word DOC APIs and other such things are all well and good as a backup plan, but let’s get serious – you want to play with the object and read the instructions only if you can’t figure out what to do. And the last thing you want to do is go reading the code of that class or, worse still, hunt down the guy that wrote it.
But, what about those times that the instantiation gets a little sidetracked? You go to instantiate the object and it’s like wandering into a Baskin Robbins knowing only that you vaguely feel like ice cream. So many flavors to choose from, but which is the right one?
In the picture above, I’ve decided I want an Aquarium object, and Intellisense informs me that there are no less than 11 ways that I can make this happen. That’s right, 11. My immediate, gut reaction to this information is to go off to implement the “AdoptADog” method instead and put this nonsense off until later.
But Aren’t More Choices Better?
With constructors, no, not really. I’ve talked before about the problem with bloated constructors and my opinion that a constructor should do nothing but ensure that the object initializes with class level variants established. With that in mind, either some of these overloads are doing more than is necessary or else some of them fail to meet this basic criteria. The former is pointless speculative coding and the latter means that your objects can be instantiated in states that are not valid. Either one of these is a problem.
I believe there is a tendency, especially if you don’t practice TDD or even write unit tests at all, to go off on tangents about how developers may want to instantiate objects. Maybe developer X will want to instantiate an aquarium with all defaults whereas developer Y will want to specify how many gallons it holds and how many fish are in it. Maybe developer Z just wants to initialize with the kind of rocks that go in the bottom or the kind of light that shines on top. Maybe everyone wants to initialize specifying salt or fresh water. Let’s think of every combination of things anyone may want to do to this object and offer them all up as constructor overloads, right?
But you know what? That’s what the public API is for with accessors and mutators. Everyone can do it that way. Save the constructor for things without which the aquarium makes no sense (e.g. capacity) and let everyone call a property setter or a mutator for the rest. C# even has some syntactic sugar for just this occasion.
If you add in a bunch of overloads, you may think that you’re being helpful, but you’re really just muddying the waters and paralyzing your clients with options. I may want to instantiate an aquarium and use it to hold a bunch of dirt from my back yard — so why I am I being offered all of these options about fish and water and aquarium plants and plastic divers? I don’t care about any of that. But, I’ll hesitate to omit it because for all I know I should instantiate the object with those things. I mean, with all of those overloads, some are probably vestigial or at least less frequently used. I don’t want to use something that might be deprecated or untested and nobody wants to maintain a bunch of methods that may never even be used.
In the end, what I’ll wind up doing is digging out the word document that describes this thing or going to the developer who wrote it and asking which one to use. And that sucks. If you offer me only one option — the minimal constructor that establishes the invariants and forces any critical dependencies on the client — I’ll use that option and go on my merry way. There will be nothing to think about and certainly nothing to read word documents or send emails about. And that is the essence of providing usable code and good abstractions.
(And incidentally, since Visual Studio 2010, C# has really taken away any good excuse for a lot of overloads with optional/default parameters).
I’ve also found that a large number of the constructor overloads I see in my day to day could just as well be replaced with the initializer syntax. If there isn’t actually any work being done with the parameter, it is just as effective to use the intializer syntax instead of writing into the constructor an extra overload that varies by only an assignment. You lose the ability to enforce that the value is set before the object is used, but if you provided an overload without the parameter you’ve most likely already bypassed that guarantee anyways.
Yeah, I think that’s exactly right. If I can instantiate with Foo(a) or Foo(a, b), then the first constructor supplies some sensible default for b (in which case the initializer syntax is fine) or it initializes in violation of its own preconditions. If the latter is true, then it really doesn’t matter — you can trigger a nonsense instantiation with or with out intializers.
I will use two constructers, one for production code and a second for testing. The testing one is usually marked internal and may pass in some loaded dictionary or other member that the test will work against. This may be crossing the line into testing private members but really I am loading private members to reduce test setup stress. I will add a comment about the constructor being for testing and expect the rest of the internal team to behave like professionals.
This isn’t personally a practice that I would follow, but I don’t see it as an abuse of overloads. If there is a “testing” constructor and a “production” constructor, it’s pretty easy to sort out which one to use in client code (especially from another assembly, where you wouldn’t even see the “testing” constructor). I’ll leave discussion of the merits of different entry points for tests for another time… 🙂
Another reason to keep it simple.
Oh God, programmers use code without reading the documentation? I pull up the man page on POSIX functions I’ve used for 15 years, JUST TO MAKE SURE I DIDN’T FORGET SOMETHING. In your example of just poking around you’re never going to know that Aquarium requires a well-defined Water object that specifies the volume of Water before constructing the Aquarium. If you don’t read the documentation you’ll never know that the default-constructed Water is unbounded and continues to fill the Aquarium until it FLOODS THE WHOLE HOUSE. You’ll only find out after your Aquarium app runs for a few days… Read more »
Hi Jonathan – thanks for reading and for the comment. What I’m addressing here is really the supply side rather than the demand side. The anecdote of instantiating an object and using Intellisense is to set the stage for who your audience is going to be when writing APIs. In my experience observing and working with others, experimentation before reading the manual is common with everything from code APIs to coffee table assembly instructions (how this approach stacks up against careful reading of documentation, disassembling third party code, etc would make an interesting discussion in and of itself and I… Read more »