Optimize For Simplicity First
Lane Wagner
Posted on August 24, 2020
The post Optimize For Simplicity First first appeared on Qvault.
We can’t optimize for everything in software engineering, so we need to start with something, and that something should be simplicity. For example, to over-optimize for speed in JavaScript, we might write our for-loops backwards to the detriment of readability. On other occasions, we may over-optimize architectural abstraction to the detriment of speed.
I assert that we should optimize for simplicity first, and only make complex memory, speed, and abstraction improvements as they become necessary.
But Muh Speed
If it’s slow but readable, I can make it fast. If it’s broken but readable, I can make it work. If it’s impossible to understand, then I have to ask around until I can find out what the abomination is supposed to do in the first place.
Working, readable software should be the “MVP” of your code. It’s trivial to find a bottleneck in code that is easy to understand. That one slow chunk of code can be optimized for speed when and if necessary.
A Small Caveat
There are cases in which it makes sense to take speed seriously upfront. For example, choosing which language or framework to use for a project is a decision that cannot be undone or changed easily.
Memory Problems
Do you need Redis? But do you REALLY need Redis? Probably not. In the case of a web API, omit caching on your first iteration. Most servers don’t require in-memory caching to effectively service users. When speed starts to become a problem, implement in-memory caching on the server itself if possible. In terms of overall system complexity, the only thing worse than code dependencies are external dependencies.
Only add a new database, queuing system, API service, or NPM module if there is no simpler option.
Abstractions and DRY Code
There is nothing wrong with writing reusable functions, and most will written functions will be reusable without adding any needless complexity. However, too often I’ve seen developers over-generalize a problem to the detriment of readability.
If there is currently only one place in your application where a function is being called, don’t worry about making that function the most generalized version of itself. For example, let’s say I have some validation middleware in my Go API:
type apiParams struct {
OrgID string
UserID string
}
func validateParams(params apiParams) error {
if params.OrgID == "" {
return errors.New("OrgID is required")
}
if params.UserID == "" {
return errors.New("UserID is required")
}
return nil
}
A useful function to be sure, but our naive DRY filter may kick in an tempt us to do the following:
type apiParams struct {
OrgID string
UserID string
}
func validateParams(params interface{}) error {
dat, _ := json.Marshal(params)
mapParams := map[string]string{}
json.Unmarshal(dat, &mapParams)
for k, v := range mapParams {
if v == "" {
return fmt.Errorf("%s not found", k)
}
}
return nil
}
We’ve succeeded in making the code more abstract, now any function can pass in any struct and check if the fields exist! The problem is that we have also added many edge cases that will certainly produce bugs under many conditions. For example, what if an integer is passed in? What if a struct that contains more than just string
values is used?
The code was just fine as it was, we had no reason to generalize it. When we finally are forced to generalize it later we will know better how to build a good abstraction.
KISS > DRY. When used properly, DRY code will be more simple than it was before anyhow, these rules aren’t in direct competition.
Thanks For Reading!
Follow us on Twitter @q_vault if you have any questions or comments
Take game-like coding courses on Qvault Classroom
Subscribe to our Newsletter for more educational articles
Posted on August 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.