9 rules for writing cleaner code
Writing code that stands the test of time isn't easy. Find out how practicing object calisthenics will improve your code's structure, readability, and performance.
Calisthenics, a term derived from the Greek words "kalòs" (beautiful) and "sthènos" (strength), refers to a form of strength training that emphasizes beauty and strength through exercises requiring minimal or no equipment.
This principle finds a parallel in software design with Object Calisthenics, a concept introduced by Jeff Bay in "The ThoughtWorks Anthology". With this adaptation, it consists of a set of nine rules aimed at enhancing code's maintainability, readability, and overall quality by imposing specific constraints.
The initial idea of the paper was to create a pet project to follow these 9 rules to the teeth, yet the way I see it is that the end goal is to assimilate them as guidelines or, better yet, reflexes, to avoid common pitfalls, promoting a more thoughtful and refined approach to coding.
These rules encourage us all to write cleaner, more readable, and more maintainable code. Again, even if they may seem restrictive at first, adhering to them will significantly enhance your coding practices.
The rules are:
Use Only One Level of Indentation per Method
Don’t Use the ELSE Keyword
Wrap All Primitives and Strings
Use First-Class Collections
Use Only One Dot per Line
Don’t Abbreviate
Keep All Entities Small
Don’t Use Classes with More Than Two Instance Variables
Don’t Use Getters or Setters
Now let's break down these 9 rules:
1. Use Only One Level of Indentation per Method
This rule enhances readability and maintainability by ensuring methods are concise and focused on a single task — kind of like reading a document with clear and distinct paragraphs.
Each method (representing a paragraph) maintains a specific level of abstraction, guiding the reader through the code's logic without overwhelming them with details.
Adopting this approach not only improves code cohesiveness and reusability, but I also found it simplifies stack tracing and eases understanding by keeping unnecessary details out of a method's concern.
You might say that this strategy makes is harder to read the whole context of the class, because you have to hop in and out of methods to understand the context. I disagree with this idea, as you are normally reading the code as though it were a set of TO paragraphs, each of which is describing the current level of abstraction and referencing subsequent TO paragraphs at the next level down.
Sticking by this rule actually facilitates a smoother navigation through code by adhering to a single level of abstraction.
It's like cooking a lasagna; you don't need to know how to make bechamel sauce from scratch if you're not planning to make it, hence keeping the focus on assembling the lasagna layers.
Can you tell I’m hungry as I’m writing this?
2. Don’t Use the ELSE Keyword
Eschewing the else
keyword not only clarifies code and reduces cognitive load, but it also helps in avoiding code duplication.
By favoring early return statements and leveraging polymorphism for conditional operations, code becomes more linear and less nested.
This practice leads to a cleaner, more intuitive code structure and reduces the cognitive load off our shoulders. Each decision point is straightforward, enhancing the code's readability and maintainability.
3. Wrap All Primitives and Strings
An int
, on its own, is merely a scalar with no meaning. It relies solely on method names to express intent.
Wrapping primitives in objects not only provides the compiler (and the developers touching the code) with more context about the value's nature and its purpose, but it also facilitates semantically correct programming.
This encapsulation allows for type hinting and the incorporation of behavior into value objects, overcoming the issue of primitive obsession.
Through encapsulation, operations related to the value are centralized, enhancing code semantics and utility.
4. Use First-Class Collections
This rule specifies that any class containing a collection should not have any other member variables. It emphasizing that each collection deserves its own dedicated home (class).
Such encapsulation turns collections into first-class citizens within the domain, providing a clear home for behaviors related to the collection, such as filtering, aggregation, or applying rules to each element.
Wrapping collections thus not only gives them a more defined structural role in the domain, but also elevates domain-specific concepts, making collections integral and well-defined components of your software's architecture.
This principle is akin to wrapping primitives, but specifically tailored for collections, ensuring that behaviors have a designated home and that collections are treated with the same care and consideration as other domain-specific elements.
5. Use Only One Dot per Line
It can be quite daunting to decide which object should take responsibility for an action.
But a great strategy to identify misplaced responsibilities is to search for lines with multiple dots, as these often indicate a violation of encapsulation or a middleman object that knows too much.
This rule underscores the importance of adhering to the Law of Demeter, advocating for direct communication with an object rather than "poking around" its internals.
Adhering to this guideline significantly improves readability and debuggability, as it isolates actions to specific lines and makes it easier to pinpoint errors. To better picture this, imagine having a trace that shows an exception on a line with 4 dots.
Exceptions to this rule include Fluent Interfaces and the Method Chaining Pattern, where the goal is clarity.
But even then, a better approach is through a "taller" rather than "wider" code structure. This means separating one dot per line to make it easier to reason about and follow.
6. Don’t Abbreviate
Naming is one of the most challenging tasks programmers face.
Abbreviations unfortunately don’t make this any easier. They obscure meaning and indicate underlying issues like duplication or misplaced responsibilities.
Resist the urge to abbreviate, as this, most often than not, leads to confusion and hides larger design problems.
Consider the reason behind the abbreviation desire; it may reveal opportunities to eliminate redundancy or indicate that responsibilities might be better allocated.
At the other end of the spectrum, avoid over bloating names unnecessarily — context can often eliminate the need for repetition. For example, within a User
class, ID
clearly refers to the user’s ID without needing further specification.
Striking the right balance in naming fosters clearer communication, reveals deeper design insights, and enhances code readability.
7. Keep All Entities Small
This guideline advocates for no class exceeding 50 lines and no package exceeding ten files. Its end goal is to promote cohesiveness and ease of understanding.
Its main suggestion is that large classes often undertake multiple responsibilities, detracting from their clarity and reusability.
As a bonus, small classes benefit from fitting on a single screen, aiding quick comprehension.
Yes, I know, nowadays we have huge monitors. But I hate squinting, thus I keep my code editor’s zoom to obnoxious percentages.
Not gonna lie though, 50 lines can get quite challenging to implement. Yet having this goalpost in mind, helps in creating small, focused classes and leveraging packages for logically grouped behaviors. And this can significantly enhance code quality.
My personal flavor of this rule though is to focus on method-level, that is aiming for 10 to 15 lines max per method, and adhering to a guideline of no more than five methods per class. I found that this helps prevent violation of the Single Responsibility Principle (SRP), avoiding "god classes" and ensuring shorter dependencies.
8. Don’t Use Classes with More Than Two Instance Variables
This rule works as a caution against mixing responsibilities. Consider it a manifesto to preserve class cohesion.
It encourages limiting classes to two instance variables to foster an effective object model and SRP adherence. Adding more instance variables generally decreases cohesion, prompting a thoughtful decomposition into a hierarchy of collaborating objects.
This principle aids in simplifying complex object models, ensuring behavior aligns naturally with the appropriate class, bolstered by the compiler and encapsulation rules.
Again, here as well I find that the rule is quite strict, but it is so for the sake of the exercise — to get you in the right mindset when writing production code.
Nonetheless we must all recognize the necessity of flexibility. Instances where more variables are logically cohesive can deviate from this strict guideline.
My personal rule of thumb is to stick to 5 instance variables, but then again, it’s a rule of thumb, not something set in stone. It’s rather something to raise your awareness and start questioning whether that class still respects the SRP principle or not.
9. Don’t Use Getters or Setters
This guideline is built directly on the principle of strong encapsulation from the previous rule. It seeks to ensure behaviors remain closely aligned with their respective instance variables, reducing direct access to object states.
This approach aims to centralize behavior in the object model, leading to less duplication, fewer errors, and easier implementation of new features.
Emphasizing the principle of "Tell, don’t ask", it encourages designing objects that manage their own data, fostering strong encapsulation.
I find this particularly beneficial in Domain-Driven Design (DDD) for modeling domain entities. When working with DTO’s or contracts from APIs, I find it harder to apply this rule. Even so though, I try to stick to private setters instead of opening the gates to unwelcomed alterations, to maintain encapsulation without overly restricting data access.
As if all of these 9 rules weren’t enough, I feel at liberty of adding a 10th one:
10. Don’t Use Comments to Justify Bad Code
Comments should not be used as a crutch for poor code; instead, the code should be refactored to address underlying issues directly.
The only exceptions for comments are to clarify unconventional decisions or to provide summaries in public libraries.
TODO’s are accepted, as long as they don’t make it to the main branch.
If it’s something that cannot be done in the current feature, add a task or a story on your project’s backlog. I found that having such TODOs in the code is simply useless, as they will just get old, unprioritized and forgotten in the code.
Conclusion
These rules, as outlined by Jeff Bay, serve as an exercise to strengthen your coding reflexes and intuition, just like physical calisthenics enhance your body’s flexibility and mobility.
While there may be contradictions or challenges in applying these rules universally, the intent is to foster a mindset conducive to writing better code.
It's about finding a balance, using the rules to guide but not constrain and integrating them as they fit comfortably into your development practices, ultimately refining your approach to coding in real-life production scenarios.
P.S. If you liked this post, share it with your friends and colleagues.
Great list Helen!
Agree with all the points except perhaps for the wrapping primitives. We did try it in one project with not-so-good results.
Maybe, it was overdone and this approach can work for a few important variables that are important value objects for the domain.