Object Calisthenics : Rule 1

Leo BchecheLeo Bcheche
5 min read

Hi Dev, have you heard about Object Calisthenics before?

Object Calisthenics is a set of nine coding rules introduced by Jeff Bay in The ThoughtWorks Anthology. The term combines "Object", referring to object-oriented programming (OOP), and "Calisthenics", which means structured exercises — in this case, applied to code. These rules are meant to help developers internalize good OOP principles by practicing them at the micro level, directly in their day-to-day coding.

You’re not expected to follow every rule 100% of the time. Instead, the idea is to use them as a guide — a structured constraint — to help shift your coding habits and improve the design of your software. As you apply these rules consistently, you’ll begin to see the benefits: better encapsulation, cleaner abstractions, and more testable, readable, and maintainable code.

Seven of the nine rules focus directly on strengthening encapsulation — one of the core principles of OOP. Another encourages replacing conditionals with polymorphism. The final rule helps improve naming clarity by avoiding cryptic abbreviations. Together, they push developers to write code that is free from duplication and easier to reason about.

At first, following these rules may feel uncomfortable or even counterproductive. But that friction is exactly the point — it forces you to break old habits and rethink how your objects interact. Over time, these small design constraints train you to write code that is simpler, more focused, and easier to evolve.

In this article, we’ll go over the general idea, and in the next ones, we’ll dive deeper into each rule.


Rule 1: Only One Level of Indentation per Method

This rule is all about reducing complexity by keeping control flow flat and your methods focused. In practice, this means avoiding nested if, for, while, and other control structures inside one another. If a method goes deeper than one level of indentation, it probably does too much.

Example 1 - Simplifying Validation with Early Returns

When performing a sequence of validations, it’s tempting to nest each check inside the previous one. But this leads to deeply indented code that’s harder to read and maintain. Using early returns keeps the logic flat and makes each condition clear and independent.

'''❌ Violates the rule'''

def validate_user(user):
    if user.is_active:
        if user.email_confirmed:
            if user.age >= 18:
                return True
    return False

Using early returns is a great way to flatten control flow. It's more readable and removes unnecessary nesting.

'''✅ Cleaner version '''

def validate_user(user):
    if not user.is_active:
        return False
    if not user.email_confirmed:
        return False
    return user.age >= 18

Example 2 - Breaking Nested Loops into Focused Functions

Nested loops and conditionals can make even simple logic look overwhelming. This example processes orders, but the logic is hard to follow due to deep nesting.

'''❌ Violates the rule'''

def process_orders(orders):
    for order in orders:
        if order.is_valid():
            for item in order.items:
                if item.in_stock():
                    ship_item(item)

Each block of logic deserves its own space. Refactoring with smaller and focused functions helps eliminate nesting and enhances both readability and reusability.

'''✅ Cleaner version '''

def process_orders(orders):
    for order in orders:
        _process_order(order)

def _process_order(order):
    if not order.is_valid():
        return
    for item in order.items:
        _process_item(item)

def _process_item(item):
    if not item.in_stock():
        return
    ship_item(item)

Example 3 - Handling Errors with Extracted Fallbacks

Handling exceptions inline with nested logic often leads to bloated and deeply indented code.

'''❌ Violates the rule'''

def load_user_profile(user_id):
    try:
        user = fetch_user(user_id)
        if user.has_profile():
            return user.get_profile()
        else:
            return create_default_profile(user)
    except UserNotFoundError:
        log_warning(f"User {user_id} not found")
        return None

This method handles several responsibilities at once — error handling, profile checking, default fallback — all in a single method with three levels of indentation.

A better approach is to separate the main logic from the fallback logic using helper functions or early exits. Breaking it up makes the flow clearer and more maintainable:

'''✅ Cleaner version'''

def load_user_profile(user_id):
    try:
        user = fetch_user(user_id)
    except UserNotFoundError:
        return _handle_user_not_found(user_id)

    return _get_user_profile(user)

def _handle_user_not_found(user_id):
    log_warning(f"User {user_id} not found")
    return None

def _get_user_profile(user):
    if user.has_profile():
        return user.get_profile()
    return create_default_profile(user)

By extracting exception handling and profile logic into dedicated helpers, you keep each function flat and focused — with only one indentation level per method.


Why This Rule Matters

  • Readability: Shallow functions are easier to understand at a glance.

  • Debuggability: Smaller chunks make it easier to isolate bugs.

  • Testability: You can unit test behavior in smaller pieces.

  • Maintainability: Less nesting means lower cognitive load when modifying logic.

  • Reusability: You now have reusable pieces like handle_permissions().


Trade-offs

  • More methods: You’ll end up with many small functions. That’s intentional — but it can feel verbose at first.

  • Naming pressure: You’ll need to give meaningful names to each method, which is hard but valuable.

  • Overhead in navigation: Jumping between small functions may feel disorienting in IDEs without good navigation support.


Practical Tip

Most modern IDEs support "Extract Method" refactoring. Use it aggressively. Also, try to limit each method to 5–10 lines max. That often naturally enforces one indentation level.


Works Great with SRP

This rule complements the Single Responsibility Principle. When a method starts nesting, it’s often doing more than one thing. Breaking nesting forces you to isolate behavior, improving cohesion and clarity.


Final Thoughts

Rule #1, “Only One Level of Indentation per Method,” is a clear example of how small constraints can lead to big improvements in code quality. By flattening your control flow and breaking logic into smaller, focused methods, you make your code easier to read, test, and maintain. Sure, it may feel awkward at first — but with practice, it sharpens your instincts for clean design.

In the next articles, we’ll keep exploring the rest of the rules, one by one, to help you level up your object-oriented thinking and bring more clarity to your everyday code.

Stay tuned!

0
Subscribe to my newsletter

Read articles from Leo Bcheche directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Leo Bcheche
Leo Bcheche