Cutting Down on Code Telepathy
Let’s say that you have some public facing method as part of an API:
public void Process(CustomerOrder order)
{
//Whatever
}
CustomerOrder is something that you don’t control but that you do have to use. Life is good, but then let’s say that a requirement comes in saying that orders can now be post-dated, so you need to modify your API somewhat, to something like this:
public void Process(CustomerOrder order, DateTime orderDate)
{
if(orderDate < DateTime.Now)
throw new ArgumentException("orderDate");
//Whatever
}
Great, but that was really painful because you learn that publishing changes to your public API is a real hassle for both yourself and for your users. After a lot of elbow grease and grumbling at the breaking change, though, things are stable once again. At least until a stakeholder with a lot of clout comes along and demands that it be possible to process orders through that method while noting that the order is actually a gift. You kick and scream, but to no avail. It has to go out and it has to hurt, and you're powerless to stop it. Grumbling, you write the following code, trying at least to sneak it in as a non-breaking change:
public void Process(CustomerOrder order, DateTime orderDate, bool isGift = false)
{
if (orderDate < DateTime.Now)
throw new ArgumentException("orderDate");
}
But then you start reading and realize that life isn't that simple and that you're probably going to break your clients anyway. Fed up, you decide that you're going to prevent yourself ever from being bothered by this again. You'll write the API that stands the test of time:
public void Process(CustomerOrder order, Dictionary options)
{
if(((DateTime)options["orderDate"]) < DateTime.Now)
throw new ArgumentException("options");
}
Now, this can never be wrong. CustomerOrder can't be touched, and the options dictionary can support any extensions that are requested of you from here forward. If changes need to be made, you can make them internally without publishing painful changes to the API. You have, fortunately, separated your concerns enough that you can simply deploy a new DLL that handles order processing, and any new values supplied by your clients can be handled. No more API changes -- just a quick update, some testing, and an explanatory Word document sent to your client explaining how to use the thing. Here's the first one:
public class ProcessingClient
{
private OrderProcessor _orderProcessor = new OrderProcessor();
public void SubmitAnOrder(CustomerOrder order)
{
var options = new Dictionary();
options["orderDate"] = DateTime.Now;
options["isGift"] = true;
_orderProcessor.Process(order, options);
}
}
There. A flexible API and the whole "is gift" thing neatly handled. If they specify that it's a gift, you handle that. If they specify that it isn't or just don't add that option at all, then you treat those equally as the default case. Important stakeholder satisfied, and you won't be bothered with nasty publications. So, all good, right?
Flexibility, but at what cost?
I'm guessing that, at a visceral level, your reaction to this sequence of events is probably to cringe a little, even if you're not sure why. Maybe it's the clunky use of a collection type instead of something slicker. Maybe it's the (original) passing of a Boolean to the method. Perhaps it's simply to demand to know why CustomerOrder is inviolate or why we couldn't work to an order interface or at least define an inheritor. Maybe "options" reminds you of ViewState.
But, whatever it is, doesn't defining a system boundary that doesn't need to change seem like a worthwhile goal? Doesn't it make sense to etch painful boundaries in stone so that all parties can rely on them without painful integration? And if you're going to go that route, doesn't it make sense to build in as much flexibility as possible so that all parties can continue to innovate?
Well, that brings me to the thing that makes me wince about this approach. I'm not a fan of shying away from the pain of "icky publish/integration" instead of going with "if it hurts, do it more and get better at it." That shying away doesn't make me wince in and of itself, but it does seem like the wrong turn at a fork in the road to what does make me wince, which is the irony of this 'flexible' approach. The idea in doing it this way is essentially to say, "okay, publishing sucks, so let's lock down the integration point so that all parties can work independently, but let's also make sure that we're future proof so we can add functionality later." Or, tl;dr, "minimize multi-party integration coordination with hyper-flexible API."
So where's the irony? Well, how about the fact that any new runtime-bound additions to "options" require an insane amount of coordination between the parties? You're now more coupled than ever! For instance, let's say that we want to add a "gift wrap" option. How does that go? Well, first I would have to implement the functionality in the code. Then, I'd have to test and deploy my changes to the server, but that's only the beginning. From there, I need to inform you what magic string to use, and probably to publish a Word document with an example, since it's easy to get this wrong. Then, once you have that document, I have to go through my logs and troubleshoot to discover that, "oh yeah, see that -- you're passing us 'shouldGiftwrap' when it should really be 'shouldGiftWrap' with a capital W." And if I ever change it, by accident or on purpose? You'll keep compiling and running, and everything will be normal except that, from your perspective, gift wrapping will just quietly stop working. How much pain have we saved in the end with this non-discoverable, counter-intuitive, but 'flexible' setup? Wouldn't it be better not to get cute and just make publishing a more routine, friction-free experience?
The take-away that I'd offer here is to consider something about your code and your software that you may not previously have considered. It's relatively easy to check your code for simple defects and even to write it in such a way to minimize things like duplication and code churn. We're good at figuring out how not to have to keep doing the same thing over and over as well and to simplify. Those are all good practices. But the new thing I'd ask you to consider is "how much out of band knowledge does this require between parties?"
It could be a simple scenario like this, with a public facing API. Or, maybe it's an internal integration point between your team and another team. But maybe it's even just the interaction surface between two modules, or even classes, within your code base. Do both parties need to understand something that's not part of the method signatures and general interaction between these entities? Are you passing around magic numbers? Are you relying on the same implicit assumptions in both places? Are there things you're communicating through a means other than the actual interactions or else just not communicating at all? If so, I suggest you do a mental exercise to ask yourself what would be required to eliminate that out of band communication. Otherwise, today's clever ideas become tomorrow's maintenance nightmares.