Understand More By Thinking Less

Cory
Nerd For Tech
Published in
6 min readMay 29, 2021
Model of a human head with the brain exposed
Photo by David Matos on Unsplash

“Knowledge is power” it is said. It is also said that “power tends to corrupt”. These axioms are true in our code as much as they are true in life. Let me explain.

When we work in our code bases, we tend to hold varying degrees of the application logic in our head at once. It helps us to understand the relevant parts of the system we are working in. This is a good thing. It is important for us to understand how a change in feature A might have an affect on feature B. Depending on how we’ve designed our application, we need to hold more or less of that knowledge in our head at once in order to be productive.

The trick to being more productive is not to hold more of that application knowledge in your head though. The trick is to create an application that requires as little information as possible to need to be held in anyone’s head at one time to be able do what they need to do.

For me, I’ve found holding lots of the application logic in active memory tends to cause me to over engineer things. In other words, while I might strive for a vast mind palace, I like to keep the desk in my mind palace small and tidy; refiling pieces of information as I finish with them before retrieving new pieces for the next problem. We have a word for this behavior in programming. It’s called encapsulation.

Encapsulation

Encapsulation is the idea that certain pieces of information are contained entirely within certain boundaries. Like medicine inside a tiny capsule.

Why do we value encapsulation in programming? It’s not so that the computer can understand it better. In fact, if anything, it likely adds a degree of overhead to the execution because now the computer has to create many more contexts and scopes to represent this encapsulation. No, we worry about encapsulation to make it easier for us to reason about the code. It’s for the next person. Code that limits the scope of a thing is easier to reason about. If we don’t give access to some piece of information or functionality to an other part of the code, that code does not need to consider that information or functionality in its logic, and nor do we.

But this idea can work the other way too. Say, for example, we need to verify the role of some user. Let’s say they need to be a contributor in order to do some certain thing in the system. We might write a validation function like this:

const isContributor(user) => user.role === `contributor`

This is a very normal and common approach. But it also encourages scope creep over time such that the isContributor function is more likely to begin to do other things as more hands touch the code base and more features are demanded.

const isContributor(user) => user.role === `contributor` || user.id === `0000001`

Now, perhaps user 0000001 has a legitimate reason to be able to do everything a contributor does, but this function is now a lie. It is no longer telling us whether a given user is a contributor, it’s telling us whether they are a contributor OR they have this very special id.

But that’s just bad programming. Who ever did that was an idiot.

— You, in your mind just now

Well, that’s a bit harsh. True, this is bad programming, but I’ve been around long enough to understand that given the right (wrong?) incentives, good programmers often write bad code. Maybe there was a hard deadline, maybe it’s the boss’s id and they demanded to be able to do something right now. Perhaps their grandmother just died and their headspace was not 100% engaged in the work. It really doesn’t matter why it happened. What matters is that it did, and does happen. Often. The question we should be asking ourselves is “how can we reduce the incentive to write code like this?”

For that, we can rely on something called “the principle of least knowledge” also known as “the Law of Demeter”.

The principle of least knowledge is that “a given object should assume as little as possible about the structure or properties of anything else (including its subcomponents).” What this means in practice is that a function or an object should be exactly the information it needs to do its job, and nothing else. So how does our previous example violate this principle?

In this case, we are passing in what we sometimes call a “domain object” It is an objet that encapsulates (there’s that word again 🤔) a single concept; a domain. In this case, it is the user domain. Sometimes these are also called “well-known objects”. In the context of our application, the concept of a user and all the properties and methods that that entails is a concept that is understood throughout the whole of the application. The justification for this is that we can use knowledge and information sharing to build out our application quicker.

There are some faults in this logic. For instance, the productivity gains decrease with the volatility of the domain. If the structure of the domain object is relatively fixed over time, there can be meaningful productivity gains. But if the domain is volatile, if we find we need to alter properties or methods fairly regularly, productivity declines rapidly. Particularly if that domain object is both widely used and highly volatile. In my experience, we often under estimate the volatility of our domain models, they tend to change far more often than we believe they do. So I’ve found domain object often to be the root cause of large merge requests because changes to those domain objects radiates out to every part of the application that relies on them.

To combat this, we can fashion our functions to take in only the information they need to do the job they are designed for.

const isContributor(role) => role === `contributor`

Instead of accepting an object with many properties, one of which is role, we simply pass in the value of role from the object.

This seems small and perhaps inconsequential, but remember back to our earlier example. When we pass in the whole user object, the temptation to inflate the scope of what the function is doing is great. After all, we are already passing the object in, what’s the harm in just plucking another property off?

Certainly this makes for an easy change right now. But we loose out on clarity and determinism. Bugs are often introduced because of bad assumptions on the part of developers. Increasing the surface area for assumptions to happen necessarily increases the surface area for bad assumptions to happen.

Here’s a real use case that happened to me recently. I refactored some code to accommodate a new feature and inadvertently introduced a bug (don’t worry, I caught it before it got to production 😅). Notice that this even had static typing. In theory this should have been caught by the compiler. I’m not entirely sure why it wasn’t caught, but it wasn’t.

const hasError = (message: MESSAGE_TYPE): boolean => (
message.status === UNKNOWN_ERROR ||
message.status === UNKNOWN_STATUS ||
message.status === ERROR
)

This code failed because message was coming in as undefined causing the application to throw causing a giant blank screen. The compiler didn’t find this, but applying the principle of least knowledge would have prevented the error from being thrown.

const hasError = (messageStatus: MESSAGE_STATUS_ENUM): boolean => (
messageStatus === UNKNOWN_ERROR ||
messageStatus === UNKNOWN_STATUS ||
messageStatus === ERROR
)

We could take this a step further and reduce a bit more surface area for bugs while also being more explicit about what our intention is:

const hasError = (messageStatus: MessageStatusEnum) =>
[UNKNOWN_ERROR, UNKNOWN_STATUS, ERROR].includes(messageStatus)

If you want to eliminate a lot of opportunity for bugs to creep in, make your code base more resilient to many hands, and help JR engineers or new hires more quickly become productive, practice the Principle of Least Knowledge. Make your data on a need to know basis. If the function doesn’t need to know it, don’t give it to it. It’s a small thing that pays dividends in sometimes unexpected ways.

There’s so much more that could be said about this, but we would start getting into a whole thing. I think it’s best to leave it here for now. Just remember. when disseminate assumptions about your application throughout your application, not only do you make it materially harder to change those assumptions as your business needs shift, in a sense, your are making your application self-aware, and we all know how that tends to go.

The Terminator
The terminator

--

--

Cory
Nerd For Tech

Front End Engineer with deep technical expertise in JS, CSS, React, Frontend Architecture, and lots more. I lean functional, but I focus on maintainability.