Object Calisthenics : Rule 4

Leo BchecheLeo Bcheche
8 min read

Introduction

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’re diving into Rule #4 – First-Class Collections.

When filtering or updating collections spreads across your code, duplication and bugs follow. This rule helps you bring that logic back under control — with cleaner, safer, and more expressive code.


Rule Definition and Explanation

First-Class Collections states:

Any class that contains a collection should do nothing else*.*
All access to the collection must go through this class.

In practice, instead of scattering list, dict, or set manipulations throughout your codebase, you wrap the collection in its own object and expose only intention-revealing methods (add_item, total_price, ordered_by_priority, …).
The wrapper becomes a domain concept rather than just “some list of things”.


Why Do the Rule?

  1. Encapsulation – Prevents external code from reaching in and mutating the list at will.

  2. Single Responsibility – The new class focuses solely on collection behavior (sorting, validation, aggregation).

  3. Rich Domain LanguageShoppingCart.add(product) speaks louder than cart_items.append(product).

  4. Testability – You can unit-test collection logic without dragging in unrelated state.

  5. Evolution – You can swap list for, say, a priority queue without touching the rest of the system.


Example 1 – Hiding Low-Level Operations

Avoid using raw lists (or other collections like dictionaries or sets) directly in your code, especially in many different places.

Instead, create a class that wraps the list. This makes your code more organized, easier to understand, and safer to change in the future.

❌ Violates the rule
orders: list[Order] = []

# in other parts of the code...
orders.append(order)
orders.sort(key=lambda o: o.date)

In this example, the orders list is accessed and modified directly in many places. The logic for adding and sorting is not controlled in one place.

Problems with this code

  • The list is used in many parts of the code: You need to remember everywhere how to add and sort orders. If the sorting rule changes later, you will have to find and fix it in many places — and that’s risky.

  • Sorting logic might be copied: If you write orders.sort(key=lambda o: o.date) in many places, you're repeating the same code. If the sorting rule changes (for example: sort by date and by priority), you must update all the places manually. This breaks the DRY principle (Don't Repeat Yourself) — in other words: don’t copy the same logic in many parts of the code.

  • The code is doing too many things at once (adding a new order and deciding how to sort the list): This breaks a good object-oriented programming idea called cohesion. Cohesion means that each part of the code should have only one clear responsibility. If one part does many different things, the code becomes harder to understand and fix.

Better way: wrap the list in a class

#✅ First-Class Collection
class Orders:
    def __init__(self):
        self._items: list[Order] = []

    def add(self, order: Order) -> None:
        self._items.append(order)

    def by_date(self) -> list[Order]:
        return sorted(self._items, key=lambda o: o.date)

Now we have a class called Orders that contains the list and knows how to use it.

Benefits of this approach

  • Only the Orders class knows how to add or sort orders. The rest of the code just calls the methods. This protects your business logic.

  • If the sorting rule changes, you only need to update it in one place.

  • The list is no longer just a simple structure of data. It becomes a domain object, which means it represents a real concept in your system — the list of orders.

Example 2 – Enforcing Business Rules (Invariants)

In the last example, we focused on hiding low-level operations like append() and sort().
This time, the focus is different:
We need to protect a rule that must always be true — this is called an invariant.

It’s not just about better structure. It’s about stopping bad data from entering the system, even by mistake.

❌ Example that breaks the rule
def place_bid(bids, bid):
    if bid.amount > max(b.amount for b in bids):
        bids.append(bid)

This looks fine, but the rule “only accept higher bids” is not protected.
Anyone can add a wrong bid if they forget the condition.

Problems with this code

  • Bad data can be saved in this case: Before, the issue was code duplication and organization.
    Now, the system can accept invalid business data — which could break a real auction or process.

  • The class must control access, not just structure: In the previous example, the class just helped organize things. Here, the class becomes a gatekeeper: it decides if something can be added.

  • The logic includes validation and return value: The method place() can refuse the bid and returns False. That means the class now includes decision-making logic, not only behavior.

#✅ Better version: encapsulate the validation logic
class Bids:
    def __init__(self):
        self._highest: float = 0.0
        self._items: list[Bid] = []

    def place(self, bid: Bid) -> bool:
        if bid.amount <= self._highest:
            return False
        self._items.append(bid)
        self._highest = bid.amount
        return True

Benefits of this approach

  • Now, only the class can allow or reject bids.

  • External code doesn’t need to worry about the rule anymore.

  • If the rule changes, you only update one place.

  • It’s very clear who is responsible for enforcing the rule: the Bids object itself.

Example 3 – Rich Queries

In the first examples, we focused on hiding low-level actions and enforcing business rules.
Now, the focus is different again:
We want to make queries in the code more clear, reusable, and easy to read.

#❌ Example with repeated filtering

active_users = [u for u in users if u.is_active]

This works fine, but this kind of logic often gets repeated in many places.

Problems with this code

  • It’s not about protection — it’s about readability and reuse: In earlier examples, we were trying to protect data or enforce a rule. Here, the goal is just to query a specific subset, and we want to avoid writing the same filter everywhere.

  • The risk is duplicated code and unclear intent : If you filter active users in many files, you repeat the same code. Also, if u.is_active shows how you're doing it, but not what you're trying to do.

#✅ Better version: use a method with a clear name
class Users:
    def __init__(self, initial: Iterable[User] = ()):
        self._items = list(initial)

    def active(self) -> list[User]:
        return [u for u in self._items if u.is_active]

active_users = users.active()

The second version is cleaner and easier to understand — like you're saying: “give me the active users”.

Benefits specific to this case

  • The method makes your code more readable and says what it does, not how.

  • If the logic for “active users” changes later, you only update it in one place.

  • It helps avoid repetition and makes future changes easier.

  • It makes the code easier to understand, especially for new developers or team members.


Why This Rule Matters

  • Readability – Domain-specific methods replace generic list operations.

  • Robustness – You funnel all mutations through a controlled API.

  • Discoverability – IDE autocompletion now shows meaningful behaviors (highest_bid, remove_expired_sessions, …).

  • Reusability – Downstream code consumes behavior, not structure.


Trade-Offs

❌ Cost✅ Payoff
Extra small classesStronger SRP & clearer intent
Slightly more boilerplateCentralized invariants & validation
Might feel “over-engineered” for tiny scriptsSaves hours when requirements evolve

Practical Tips

  1. Start where pain exists – Wrap collections that already have duplicated logic or tricky rules.

  2. Expose intentions, not structures – Prefer contains_email(email) over return email in self._items.

  3. Keep it lightweight – Often a thin façade with 3–6 methods is enough.

  4. Immutable where possible – Return new collections (or tuple) instead of raw internal lists.


Works Great With…

  • Rule #3: Wrap All Primitives and Strings – Together they move you toward ubiquitous, self-documenting domain types.

  • Rule #7: Keep All Entities Small – A focused collection object naturally stays tiny.

  • SRP & Tell-Don’t-Ask – Consumers tell the collection what to do, not ask what it has and decide outside.


Final Thoughts

Making collections first-class citizens may feel like an extra step today, but tomorrow it pays back with code that models the problem space instead of raw data structures.
Try refactoring just one list or dict into its own class this week—you’ll quickly see how much accidental complexity disappears.

Keep flexing those Object Calisthenics muscles, and stay tuned for the next rule!

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