Imagine, for a moment, that you are standing at an ATM. ATMs are pretty much the definition of black boxes, right? You put in your card and your PIN, you ask for $20, and a few seconds later a $20 bill pops out. You have used this particular ATM for years — it’s one of the old-school ones with a yellow CRT with so much burn-in that you can barely read it.
Today you follow the same ritual: you put in your card and your PIN, you ask for $20, and it gives you $20. But today, instead of a $20 bill, the machine gives you $20 worth of quarters.
Has the contract between you and this particular ATM been broken?
Some might argue that it has not — the machine gave you $20, which is exactly what you asked it to do. Did you see a menu for cash or coin? No? Well then, carry on.
Oh, and if you happen to not have the pockets to carry away 80 quarters … that’s not the ATM’s problem.
Let me flip this around.
You live in an urban environment where it doesn’t make sense to drive to work. You take the public transportation — bus or train or something. Each day you drop $2 worth of loose change or bills into the slot and you ride to work. Today, however, the machine’s bill reader is broken — you can only use coins. Or maybe the coin bucket is full and the machine will only accept bills.
Again: has the contract been broken?
Obviously, my point here is about determinism and meaning. Determinism and meaning are fundamental parts of any API. Once you expose anything to public consumption, be it a function or property or whatever, you cannot change anything about it without possibly breaking that determinism. Even superficial or interpretive changes still break the API — you can’t change formatting, the number of decimal places, character set, etc.
In the examples above, sure, the letter of the API has not changed. But the meaning certainly has — you’ve gone from “I expect to get a $20 bill out of this machine because that is what it has always given me” to “wtf do I do with 80 quarters?”. You didn’t get what you expected to get, and the machine has thrown determinism out the window — you now have no idea what to expect next time. What if it’s pennies? Or Russian kopeks?
Encapsulation is meant to hide things like “did I store it in a database or a text file?” or “did I use a quicksort or a bubblesort?”. It is absolutely not meant to translate to ”you’ll take what I give you and like it”. Encapsulation is a two-way street.
It’s nice and easy to say “a number is just a number” and that it should be treated opaquely, but that’s just self-deception. You can’t possibly know what meaning or value I place on that number, or anything about it. Maybe because the first time you gave it to me I saw that it had only 2 decimal places I made the logical assumption that it would always be 2 decimal places. My code now depends on that. Or that sales tax wasn’t included. Or that there was or was not a dollar sign in front of it. Or that it was in US dollars to begin with.
If I put A and B into a function today and get out C, then put them in again tomorrow, I expect to get C again. Not “c”. Not C. Exactly C and nothing else. Just like I shouldn’t care how you got C, you shouldn’t care what I do with C once you’ve given it back to me.
And that’s the kicker — C doesn’t necessarily have to be the exact same value each time, but it does have to have the exact same meaning each time.
You can’t just add or remove what goes into making C without thinking of the consequences — because you don’t know whether I depend on those things being in there or not. Then you become the one who is making assumptions, not me. At first glance it may look like I am breaking encapsulation — because I know what goes into making C — but it’s not. Encapsulation is about hiding implementation, not about hiding meaning.
In a coding world, all of this means that once I start consuming the result of foo() we have a contract. I know that it is tempting to say that foo() will always be foo(), but now the new version foo(bar) does extra special magic. But you’ve gone and changed the meaning of “foo”, haven’t you? Maybe you’ve just refined the meaning, but you’ve still changed it.
In ColdFusion and other loosely-typed and variable-argument languages it’s even more tempting. The magic of argumentCollection and structKeyExists(arguments,'foo') are just sitting there, waiting to be used, right? If you pass in this
magical incantation specific set of arguments then “foo” means one thing, but this other magic word set of arguments allows it to mean something new and different.
But wait — the meaning that underlies the trust between your code and mine, where did it go? Oh well. At least we’ve saved ourselves from having to type a little. Sure, we can’t really tell what the code means anymore, but it’s a small price to pay for being able to churn out code faster.
I just hope I remember the right magic words. I wouldn’t want to get eaten by a grue. Or have to carry around 80 quarters.