Clarity
Clear Is Better Than Clever
For most developers, the temptation to write clever, intricate code can be strong, especially for experienced engineers who enjoy pushing the boundaries of a language or framework. However, the long-term health of a codebase depends far more on clarity than on cleverness. Clear code is not just a matter of personal style; it’s a foundational principle that supports maintainability, collaboration, and the overall success of a project.
Code is read far more often than it is written. While clever code might showcase a developer’s skill or deep understanding of a language, it often comes at the cost of readability. When code is clear, what any given segment of it does should be immediately obvious to anyone who reads it, regardless of their familiarity with the specific implementation. This is crucial in a team environment, where multiple developers may need to understand, review, or modify the same code over time. Clear code reduces the cognitive load on developers, making it easier to onboard new team members and to revisit code months or years after it was written.
Clever code often relies on language-specific tricks, terse expressions, or unconventional patterns. While these may be elegant in isolation, they can obscure the underlying logic and make it difficult to trace the flow of data or identify the source of bugs. Clear code, by contrast, lays out its logic in a straightforward manner, making it easier to spot errors and reason about behavior. This leads to faster debugging and fewer regressions, ultimately saving time and reducing frustration for the entire team.
Software development is never a solo endeavor. Even if you’re working on a project alone now, it doesn’t mean that will always be the case. Teams thrive when everyone can contribute to and understand the codebase. Clever code can create bottlenecks, where only the original author fully understands how a particular piece works. This can slow down development, hinder code reviews, and increase the risk of introducing bugs during future changes. Clear code democratizes knowledge, enabling more effective collaboration and smoother handoffs between team members.
Occam’s Razor and the Dunning-Kruger Effect
Abstraction
Abstraction is a powerful tool in software engineering, allowing developers to manage complexity and build reusable components in a way that makes the code using the abstracted functionality a little clearer but often comes at the cost of increasing the cognitive load a developer is required to carry while reading the code. Moreover, unnecessary or premature abstractions can introduce their own set of problems.
Every layer of abstraction hides some detail of the underlying implementation. While this can be beneficial when managing complexity, it can also obscure the true intent of the code. Developers may find themselves navigating through multiple layers just to understand a simple operation, not only slowing them down but also increasing the risk of introducing subtle bugs that may go undetected during reviews or initial testing.
Unnecessary abstractions can make the codebase more fragile. Changes in one layer may have unintended consequences in others, and debugging becomes more challenging as the call stack grows deeper. Additionally, abstractions can sometimes introduce performance overhead, especially if they are not carefully designed.
Premature abstractions simply move infrequently used functionality from one segment of the code to another.
When drawing a line for premature or unnecessary abstractions is rather simple at the lower end of the scale: if the functionality you are abstracting is one or two lines long and is only used once or twice, it’s too early to put it in to its own function or variable.
Beyond that it become less clear-cut but - as was famously said of “obscenity” You’ll know it when you see it.
Examples
unnecessary-iota-enum.go
1// Abstraction to an integer to prevent using invalid values
2type EnforcementLevel int
3
4const (
5 Strict EnforcementLevel = iota
6 Warn
7 Lax
8)
9
10// String is used to get a string representation of the EnforcementLevel for errors or printing out
11func (e EnforcementLevel) String() string {
12 switch e {
13 case Strict:
14 return "Strict"
15 case Warn:
16 return "Warn"
17 case Lax:
18 return "Lax"
19 default:
20 return "Unknown"
21 }
22}better-enum.go
1// EnforcementLevel will be printed out as a string automatically
2type EnforcementLevel string
3
4const (
5 Strict EnforcementLevel = "Strict"
6 Warn EnforcementLevel = "Warn"
7 Lax EnforcementLevel = "Lax"
8)
9
10// If you really want to enforce the validity of the enum and need to check it in more than a handlful
11// of places, you can add a function like the one below, but if it is only checked in one place, do
12// the check there.
13func (e EnforcementLevel) IsValid() bool {
14 return slices.Contains([]EnforcementLevel{ Strict, Warn, Lax }, e)
15}Examples - Premature Abstractions
handlers/premature-abstraction.go
1func ProcessWebhook(
2 ctx context.Context,
3 request models.WebhookRequest
4) ( // return
5 response models.WebhookResponse,
6 fault error
7) {
8
9}models/better-inline.go
1func ProcessWebhook(
2 ctx context.Context,
3 request models.WebhookRequest
4) ( // return
5 response models.WebhookResponse,
6 fault error
7) {
8
9}