Objects and Classes: Identity, Equality, and State

TL;DR / Key Takeaways

  • Objects have identity and behavior; classes define the blueprint. Equality may or may not coincide with identity.
  • Distinguish entities (identity across changes) from value objects (defined by value). This choice drives equality, hashing, and mutability.
  • Keep invariants explicit; state changes should preserve them. Prefer small, focused objects with clear responsibilities.

Introduction

Object‑oriented programming starts with clear mental models: what an object represents, how we compare it, and how its state evolves. We’ll establish practical distinctions—identity vs equality, value vs reference semantics—and show runnable examples and class diagrams you can adapt. Related reads: Principles & Practices (SOLID, SoC), Modeling & UML basics.

Background

  • Identity: Two objects can be equal in value yet have different identities (two different invoices with the same total).
  • Equality: define __eq__ to capture semantic equality; match __hash__ when using in sets/dicts.
  • Value vs Entity: value objects are immutable and equal by fields; entities are equal by identity (id/UUID) and can mutate.

Deep Dive

Class Diagram

classDiagram
  class Money {+amount:int\n+currency:str}
  class Customer {+id:str\n+name:str}
  class Invoice {+id:str\n+lines: List~Line~\n+total(): Money\n+add_line()}
  class Line {+sku:str\n+qty:int\n+price: Money}
  Money <.. Line
  Invoice "1" --> "*" Line
  Customer --> Invoice

Value vs Entity in Python (runnable)

from dataclasses import dataclass
from typing import List

@dataclass(frozen=True)
class Money:
    amount: int
    currency: str

@dataclass(frozen=True)
class Line:
    sku: str
    qty: int
    price: Money

class Invoice:
    def __init__(self, id: str):
        self.id = id
        self._lines: List[Line] = []
    def add_line(self, line: Line):
        assert line.qty > 0
        self._lines.append(line)
    def total(self) -> Money:
        cur = self._lines[0].price.currency if self._lines else "USD"
        amt = sum(l.price.amount * l.qty for l in self._lines)
        return Money(amt, cur)
    def __eq__(self, other):
        return isinstance(other, Invoice) and self.id == other.id
    def __hash__(self):
        return hash(self.id)

m1 = Money(500, "USD"); m2 = Money(500, "USD")
assert m1 == m2  # value equality

i1 = Invoice("inv-1"); i2 = Invoice("inv-1")
assert i1 == i2 and (i1 is not i2)  # entity equality by identity field

Equality and Hashing Contracts (runnable)

# Using objects as dict keys requires consistent __eq__/__hash__ contracts
catalog = {Money(100, "USD"): "fee"}
assert catalog.get(Money(100, "USD")) == "fee"

seen = {Invoice("a"), Invoice("a")}
assert len(seen) == 1

Invariants and State Transitions (runnable)

# Invariant: total must be non-negative and currency consistent
inv = Invoice("inv-2")
inv.add_line(Line("SKU1", 2, Money(300, "USD")))
inv.add_line(Line("SKU2", 1, Money(200, "USD")))
tot = inv.total()
assert tot.amount == 800 and tot.currency == "USD"

Sequence of Object Interactions

sequenceDiagram
  participant U as User
  participant S as Service
  participant I as Invoice
  U->>S: create_invoice()
  S->>I: add_line(Line)
  I-->>S: total()
  S-->>U: amount

Best Practices

  • Be explicit about equality and hashing; choose value vs entity semantics deliberately.
  • Keep value objects immutable; enforce invariants on construction.
  • Design small methods with clear postconditions; validate inputs/outputs.
  • Prefer composition for behavior reuse; keep inheritance shallow.

Real‑World Scenarios

  • Using value objects for money, coordinates, and ranges to avoid unit and rounding errors.
  • Using entity identity (UUID) for long‑lived domain objects like Orders or Customers.

Measuring and Signals

  • Bug patterns: incorrect equality/hash causing duplicate keys or set membership issues.
  • Mutability leaks: unexpected state changes across references; test by defensive copying.
# Simple mutability leak check
def add_name(names: list[str], name: str) -> list[str]:
    out = names.copy(); out.append(name); return out

a = ["x"]; b = add_name(a, "y")
assert a == ["x"] and b == ["x","y"]

Implementation Checklist

  • Decide value vs entity for each concept; define equality/hash accordingly.
  • Enforce invariants in constructors; make value objects immutable.
  • Add unit tests for equality/hash and state transitions.

Conclusion

Clarity on identity, equality, and state turns OO designs into robust, predictable systems. Start with the semantics you want, and let those choices guide your implementations.

Series Footer

This article is part of the OOP Mastery Series. Next: “Encapsulation, Abstraction, Interfaces vs Abstract Classes”.

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe