
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”.