Skip to main content

Command Palette

Search for a command to run...

Object Calisthenics Concepts

Writing Better Object-Oriented Code Through Discipline

Updated
4 min read
M
Data Platform Engineer by choice.

Object Calisthenics is a set of 9 coding rules meant to improve object-oriented design, code readability, and maintainability.

It’s not a framework or a library.
Think of it as discipline for writing clean OOP code, similar to how physical calisthenics train your body.

The concept comes from “The ThoughtWorks Anthology” by Jeff Bay.

These rules are intentionally strict. Their goal is not to be followed forever, but to train your design thinking and expose weak abstractions in your code.

1️⃣ Only one level of indentation per method

❌ BAD

def get_user(user):
  if user_exists(user):
    if user_logged_in(user):
      return True
  return False

✅GOOD

def get_user(user):
  if user_exists(user) and user_logged_in(user):
      return True
  return False

Explanation

Deep nesting hides intent.
When you see multiple levels of indentation, your brain has to track too many conditions at once.

This rule forces you to:

  • Simplify conditions

  • Extract logic into methods

  • Make code easier to read at a glance

Less indentation usually means clearer logic.

2️⃣ Don’t use the else keyword

❌BAD

if user_exists():
    process()
else:
    error()

✅GOOD

if not user_exists():
    error()
    return
process()

Explanation

else tightly couples two branches of logic.
When requirements change, both branches usually need updates.

By returning early:

  • The unhappy path is handled first

  • The main logic stays flat and readable

  • The function becomes easier to extend

This pattern reduces mental load when reading code.


3️⃣ Wrap all primitives and strings

❌BAD

price = 100

✅GOOD

class Price:
    def __init__(self, value: int):
        self.value = value

Explanation

Primitives like int and str have no behavior.
They allow invalid values and spread rules across the codebase.

Wrapping primitives:

  • Gives meaning to the value

  • Allows validation in one place

  • Keeps business rules close to the data

This avoids repeating checks everywhere.


4️⃣ First-class collections

❌BAD

orders = []
orders.append(order)

✅GOOD

class Orders:
    def add(self, order):
        self._orders.append(order)

Explanation

If a collection has meaning, it should own its behavior.

Loose collections:

  • Scatter logic across files

  • Make it unclear who owns the rules

A first-class collection:

  • Encapsulates behavior

  • Protects invariants

  • Makes changes easier later

The collection becomes part of your domain model.


5️⃣ One dot per line

❌BAD

order.customer.address.city

✅GOOD

order.customer_city()

Explanation

Chained calls create tight coupling between objects.

This rule follows the Law of Demeter:

  • An object should talk only to its direct collaborators

  • Internal structure should stay hidden

It makes code less fragile when internals change.


6️⃣ Don’t abbreviate

❌BAD

cfg = Config()

✅GOOD

configuration = Configuration()

Explanation

Code is read more than it is written.

Abbreviations:

  • Save seconds when typing

  • Cost minutes when reading

Clear names improve understanding and reduce mistakes.


7️⃣ Keep entities small (≤ 50 lines)

If a class is large:

  • It has too many responsibilities

  • It’s lying about its name

Why

Small classes:

  • Are easier to test

  • Are easier to reason about

  • Encourage good separation of concerns

If a class grows too big, it usually wants to be split.


8️⃣ No classes with more than 2 instance variables

❌BAD

class User:
    name
    email
    password
    role

✅GOOD

class Credentials:
    email
    password

Explanation

Many fields often mean:

  • Procedural thinking inside a class

  • Multiple responsibilities mixed together

This rule forces you to:

  • Identify hidden concepts

  • Break large entities into focused objects

It leads to clearer domain models.


9️⃣ No getters/setters (Tell, don’t ask)

❌ (Logic Outside Class) BAD

if user.get_balance() > 100:
    user.set_status("premium")

✅GOOD

user.promote_to_premium_if_eligible()

Explanation

Getters and setters move logic outside the object.

This creates:

  • Anemic domain models

  • Business logic spread across the system

Instead:

  • Tell objects what you want

  • Let them decide how to do it

Behavior belongs inside the object that owns the data.


For any questions, discussions, or collaborations, feel free to reach out at m.safi.ullah@outlook.com.