Ironically, not only are both approaches problematic, but they share a reason for being so. They both could do with the advice: write code more declaratively and less imperatively.
For example, ternary expressions are declarative, because they just say phoneNumber = a ternary expression
, whereas an if/else that assigns to phoneNumber
in each branch is imperative about how we branch. Yes, the two approaches are logically equivalent, but the advantage of declarative code is chunking, which humans reading, writing and editing code need dearly. Chunking is when people define a concept in terms of others so they can reduce the number of concepts they must simultaneously consider, which improves what we can do with 7 plus-or-minus 2 concepts.
But ternary expressions aren't the only way to wrap up imperative logic declaratively. One of the reasons functions are so useful in programming is that invoking functions allows an arbitrary amount of imperative logic to be summarized with a single declarative line. In fact, ternary expressions are invoking a "function", just a function split across two operators. In an alternate history or another language, the syntax would be the function-like ifthenelse(headers.resourceId, headers.resourceId, DEV_PHONE_NUMBER)
. Why did language designers ever give us either that or an operator-based alternative? Because it's a useful bit of declaration. Call it syntactic sugar if you like, but its motives are deeper than that.
But the advantage to making the boolean you check a function call is that you can declare isApplicationInProduction(headers)
rather than imperatively writing either headers.resourceId
, or _.has(headers, 'resourceId')
, or whatever implementation detail gets the answer. You don't care how it's done; you only care that, if the application is in production, you use the resource ID, otherwise you use the developer's phone number. This suggests a compromise between your and your boss's code,
function isApplicationInProduction(headers) {
return headers.resourceId;
}
phoneNumber = isApplicationInProduction(headers) ? headers.resourceId : DEV_PHONE_NUMBER;
Depending on the language, we can do it all nicely in one line with no function at all, using a null-coalescing operator:
phoneNumber = headers.resourceId ?? DEV_PHONE_NUMBER;
That's a nice-looking operator. Why was it invented? Well, it's another example of chunking: a ?? b
declares an outcome achieved imperatively by checking whether a
is null, returning a
if it isn't, but otherwise returning b
. Notice in these examples why I said to write more declaratively and less imperatively, rather than declaratively and not imperatively: the distinction is not only a spectrum, but a nested one.
A 300-line function is a really bad idea from this perspective. What does such a function do? Well, this and that and that and that and that. OK, name those activities. No doubt in such functions you've seen "paragraphs", headed by comments and separated by blank lines. Those paragraphs deserve to be functions; and if you make them functions, each replacement of one paragraph with one function call replaces imperative guts with declarative description.
This idea that declaring something that does imperative work behind the scenes goes much, much further than examples like this. It's what happen every time you import from a library, let a language hand work to another, etc. It's also the reason for various OOP principles like encapsulation and the law of Demeter. We ask that code calling instances of classes only have access to what the instance class guarantees in its contract, not because we're worried that a.b.c
being legal is less secure, or anything like that, but because classes readily change how they get their job done. Going back to the code I suggested without a null-coalescing operator, it allows how we check whether the application's in production to be uncoupled from the fact that we're checking. Sure, all one approach does compared with another is rearrange the code, putting the logic on one line instead of another. But that's refactoring for you.
You'd be amazed how much of clean code boils down to making code more declarative - in practice, recursively so. Once you have an eye for it, you'll see it everywhere. True, you'll see imperative code wherever there's a for loop; but you'll also see declarative code in alternatives to it. Quite apart from the examples above (functions, and ternary and null-coalescing operators), if you've ever used SIMD calculations or a map or a filter or a Python comprehension or LINQ in C#, you've seen declarative code in action.
isApplicationInProduction()
function! You must have tests, and tests are useless if your code behaves anything different from when it is production. It's like intentionally having untested/uncovered code in production: it doesn't make sense.