KISS (Keep It Simple, Stupid!). Do the simplest thing that works. Occam’s Razor. If you’ve been writing software for many years you might be familiar with the desire for simplicity in code and system architecture. Personally, I didn’t start out with a specific appreciation for simplicity. My journey was something like:
- Look, my code works!
- Look how clever this code is!
- Never mind, look how beautiful this code is!
- Never mind, look how simple this code is!
- WTF does “simple” even mean??
That whole process took nearly 10 years. Quite recently, at step 5 I realized that my operational definition of simplicity was at best vague, and at worst a conflation of two different definitions of “simplicity”. In what is likely to be a misappropriation of philosophical terms, let’s call these two contrasting ideas Elegance and Ontological Parsimony.
A brief aside
Is this just pedantry? No. Vague words encourage vague thinking; precise words encourage precise thinking. When designing any kind of complex system, precise thinking is invaluable. So grab a drink; this’ll be a long one.
Elegance
This term should be a familiar, if seldom defined. In general it describes a minimalist form. In software engineering it includes practices like DSL’s, convention over configuration, and metaprogramming. Any code that achieves conciseness, yet avoids terseness, is likely to be considered elegant. Aesthetics, in some sense. There is absolutely nothing wrong with elegant code. But if that’s the only kind of simplicity you aim for, well, it can quickly become an anti-pattern.
Ontological Parsimony
Yeah, I know. New words are scary. But consider this: at the end you’ll have entirely new ways of thinking about systems. And thinking is most of what you do - not typing.
Ontology: In CompSci/InfoSci, an ontology is simply a listing and description of all entities in a given domain or system, and how they relate. (Think that’s abstract? Try grokking the pure philosophical definition.) For instance when designing and/or coding business processes, it’s helpful to identify all relevant entities and how they relate (Customer, Purchase, Service, Order, etc). Or at a code level, you might invent an ontology to describe the subsystems, libraries, and system dependencies which comprise your application.
Parsimony: From Merriam-Webster:
1a the quality of being careful with money or resources; 1b the quality or state of being stingy; 2 economy in the use of means to an end.
Ontological Parsimony: This is Occam’s Razor. Attributed to 14th century English monk and philosopher William of Ockham, it is often poorly summarized as The simplest explanation is usually right. A more instructive summarization would be Do not multiply entities beyond necessity. Stated this way, it is used by philosophers and scientists to choose the best (starting) explanation for a phenomenon.
Let’s say there were 12 murders in a city. Would the police immediately begin looking for 12 murderers? That’s a lot of murderers to posit, not to mention investigate and find. They would be smart to start by assuming only one murderer, then see if the crimes can be linked. If they can’t all be linked, then sure maybe they should consider additional murders and motives. But only by the necessity of the simpler One Murderer Theory not panning out.
Or take all natural phenomena. How can we explain it? Maybe there’s a rain god, a lightning god, a god of war, a sun god, and so on. That’s a whole lot of entities to assume. Why not assume a single god who handles everything? But a god is still a pretty complicated thing with intelligence, self-awareness, desires, magic powers, and presumably an entirely separate plane of existence on which to live. What if we don’t assume any gods, but instead posit that the entire universe operates solely off of some basic principles we’ll call “physics.” Maybe we’ll be wrong about physics and find that it was invented by magical deities just to take some of the load off. But physics is a good starting point because it can more easily be investigated, proven, or disproved.
So that’s the explanatory power of ontological parsimony. In software development, it’s an invaluable tool when debugging. But that’s another post. This post is about designing ontologically parsimonious software.
Ontologically Parsimonious Software
Let me admit right now that I can’t tie this up in a neat bow for you. I’m still figuring this out, and I always will be. But here’s where I’m at right now.
Have a high bar for adding new dependencies. So you need to add WebSockets to your web app. Your first instinct is to reach for socket.io. It’s easy to use, right? And it has all that fall-back functionality. But do you really need to add another dependency and increase the scope of your app’s “ontology?” Possibly. But consider that your runtime environment, the browser, already has a built-in WebSocket API. All browsers do these days; you don’t even need all those fall-backs. You’ll also need to keep it up to date, and update your app code to fix any incompatibilities with the new version. And last time I checked, socket.io wasn’t known for its small download size. How many lines of code would it take you to simply use the built-in WebSocket API. 50? 100? Certainly no more than a few kBs worth.
So sure, maybe you’ll find a solid reason to add dependency X. If it involves complicated math, or would take weeks, months, or years to write and test yourself, or you have an extremely tight deadline, or it’s anything involving encryption, then by all means add it. Just make sure that your decision to pull in another dependency, and all of its dependencies, is conscious one. Someone else wrote them. But they’re an integral part of your app’s world now, and in that sense you’re responsible for them.
Keep functional areas strictly separated. This is nothing more than writing highly modularized code. The Single Responsibility Principle, in other words. An area of code or functionality should only depend on, know about, or have access to the bare minimum of “entities” necessary to carry out its function. Does every area of your webapp really need intimate knowledge of your User
model? Probably not. And when you need to move your users and authenticate to an LDAP store, you’ll come to regret that tight coupling. I’ve always appreciated this principle in theory, but have struggled with exactly how to utilize it. Why? Because our MVC frameworks are telling us how to organize our code, and they’re wrong.
Rails, for example suggests, “Put database code here, and things that receive and respond to HTTP over here.” Organizing code by type. Really?? I would love to see DHH’s closet and dresser. I imagine he has a drawer for “cotton things”, while an area of his closet is dedicated to “items containing polyester.” Clearly that’s absurd. DHH’s clothing is doubtlessly organized along functional lines, like yours and mine. The interesting thing is, no one ever told us to do that. It’s simply obvious to anyone who’s spent any amount of time wearing clothes. That’s why we all have sock drawers instead of cotton drawers.
When I organize my code the same way, loose coupling is much easier to achieve. Billing code? It’s in app/features/billing
. User and authentication code? It’s in app/features/auth
. This is already considered best practice in Angular. It might look a little different framework to framework. Or not. Which brings us to…
Keep your functionality strictly separated from your framework, libraries, and external systems. I once saw a talk entitled The Framework Ate My Application. I didn’t hear the talk; I literally just saw that there was a talk with that name. Nevertheless it got me thinking. The most important thing in our applications is that special business logic that makes it different from other apps. Where is that? In many of my apps, it didn’t really “exist” anywhere. It “emerged” from the interactions among various models, modules/concerns, and occasionally service objects. Even if I grouped those elements by feature/functionality rather than type, the app’s behavior would still be intimately tied to the framework. Meaning that when the ontology of the billing system is described, it has to include everything from <insert framework>.
Let’s assume Rails. Practically speaking, that means your billing logic code makes all manner of implicit assumptions about the Rails version, Rails’ behavior, and whatever interesting features or unfortunate limitations Rails boasts. Important events might be triggered solely by ActiveRecord
callbacks. Vital security checks might be encoded as ActiveRecord
validations or controller before_filter
’s. And when you upgrade Rails, these interfaces might change in subtle and difficult to detect ways. Worse, when The Big Rewrite comes and you try to move your billing code to a Go microservice, you’ll have to parse out how all those models, callbacks, and validations came together to describe the billing functionality. Because it’s extremely unlikely those same interfaces and abstractions will map cleanly to whatever libraries you’re using in Go. Even if your code always lives in Rails, you’ll find yourself telling the business side, “That change is going to be really difficult because technical-bullshit-rails-has_many-and-strong-params-STI.” Even I cringe when I hear myself say shit like that, so I can only imagine how they feel.
What if that billing logic was implemented in Plain Old Ruby Objects and classes? Maybe they pass around some AR objects. But those models are merely a means of reading and writing data - they don’t contain logic or behavior. (Some would even say passing around Structs is a superior approach, allowing you to completely hide your ORM; YMMV.) Your controllers might validate the session and grab needed parameters from the request. But to do anything they’ll call your plain old Ruby logic classes. They won’t even know they’re running in a Rails controller. They might just as well be running in a Grape API, a Sidekiq job, a MiniTest test, or from the Rails console. And when it comes time to upgrade Rails, all that important code should be very low-touch. Because it doesn’t live inside the framework. Your code exists on its own, and some framework just happens to be hooking it into an HTTP request/response cycle.
This is hardly a new idea. But for whatever reason I didn’t fully grasp it until approaching it from the perspective of preferring small ontologies over sprawling ones.
Final Thoughts
If you’ve made it this far, congratulations. You’re a real trooper, and we can be friends. By no means am I prescribing this as the way to build your Web app, API, or SPA. (Though some of what I said is considered best practice in parts of the Angular community.) I am being descriptive about some ways of thinking that have, so far, proven tremendously helpful to me over the past few months as I’ve worked on some reasonably complicated code, system integrations, and Functionality That Can’t Go Wrong. And testing. Writing tests for this kind of code has been an absolute delight.
And to any former philosophy students turned developers: please forgive my ham-handed attempts to justify my thinking with a lack of understanding befitting an undergrad on his first day of a required course in Intro to Philosophy. But in my defense, my Intro to Philosophy course was taught by a professor who’d lost the ability to store short-term memories in a car accident, and our course material consisted mostly of C.S. Lewis. True story.