FREE LESSON · Programming & program design · 6 OF 8
Objects, invariants, and substitutable behavior
Objects, invariants, and substitutable behavior: Use objects to protect state and make interfaces predictable.
Prerequisites: Functions, mutation, abstract data types, and exceptions.
Learning objectives
- Define an object invariant and preserve it across methods.
- Separate class-level behavior from per-instance state.
- Recognize when inheritance violates a caller contract.
An object combines state with permitted operations
A class describes how instances behave. Each instance can hold its own state, while methods implement operations on that state. The value of this organization is not that it resembles the real world. It is that a small interface can preserve a useful invariant across changes.
Consider a bounded meter whose value must remain between zero and a positive limit. Construction establishes the invariant. Each method must either preserve it or fail without leaving an invalid partial update.
class Meter:
def __init__(self, limit):
if type(limit) is not int:
raise TypeError("integer limit required")
if limit < 0:
raise ValueError("negative limit")
self._limit = limit
self._value = 0
def add(self, amount):
if type(amount) is not int:
raise TypeError("integer amount required")
proposed = self._value + amount
if amount < 0 or proposed > self._limit:
raise ValueError("outside capacity")
self._value = proposed
def value(self):
return self._value
m = Meter(5)
m.add(3)
assert m.value() == 3
The check uses a proposed value before changing state. If an addition exceeds capacity, the previous value survives. The input checks require actual integers, excluding fractional values, non-finite floats, and Booleans. An underscore expresses a Python convention for internal attributes, not an enforced security boundary.
Interfaces matter more than inheritance trees
Two implementations can satisfy the same interface without sharing a base class. Conversely, inheriting methods does not automatically preserve behavioral compatibility. If callers expect a queue's remove operation to return the oldest item, a subclass returning the highest-priority item violates that expectation despite having the same method name.
Substitutability means a replacement supports the guarantees callers rely on. It must not unexpectedly demand stronger preconditions or provide weaker postconditions. Error behavior, mutation, ordering, and sometimes performance are part of the observable contract. A shared method signature alone is insufficient.
Composition often offers a simpler relationship: an object delegates to a contained implementation through a narrow interface. For example, a logging wrapper can record successful operations around an existing store without inheriting all of that store's internal machinery. The wrapper still needs to preserve semantics, particularly when logging itself can fail.
Test object behavior through sequences, not only isolated calls. Construct two instances to detect accidentally shared mutable class state. Try an invalid operation between valid ones and verify the invariant survives. Inspect whether returned collections expose internal storage. Object-oriented design earns its complexity when it makes valid states easier to maintain and invalid transitions harder to express.
Practice problem
Specify a seat counter with reserve(n), release(n), and available(). Its capacity is fixed at construction. Write the invariant using capacity and reserved seats, and describe checks that must occur before mutation. Provide an operation sequence that tests a failed reservation leaves the state unchanged. Then consider a subclass whose reserve(n) silently reserves fewer seats when capacity is insufficient. Explain whether it can replace an implementation whose contract promises all requested seats or an exception.
Worked solution
Require integer capacity >= 0 and maintain 0 <= reserved <= capacity. Reserve requires a positive integer n and enough available seats; release requires a positive integer n no larger than reserved. Validate before changing reserved, so rejected operations preserve state. With capacity five, reserve three, attempt to reserve three more and expect failure, then confirm available is still two. Release one and confirm available becomes three. The partial-reservation subclass violates the promised all-or-failure behavior: callers may proceed believing an entire group has seats. It needs a different interface reporting the actual reservation amount, or it must preserve the original contract. Also test two independent counters and invalid negative or noninteger arguments according to the declared input policy.
Check your understanding
Why validate a proposed state before assigning it?
- It keeps a rejected operation from leaving the object in an invalid partial state.
- It makes every operation constant time.
- It prevents every possible concurrent race.
Explanation
Validation-before-mutation preserves the invariant on ordinary failure; concurrency may require additional synchronization.
A subtype changes FIFO removal to highest-priority removal with the same method signature. Is that sufficient for substitution?
- Yes, signatures completely define behavior.
- No, it changes an observable ordering guarantee.
- Yes, if its code is shorter.
Explanation
Clients depend on behavioral contracts, including ordering, not just names and parameter lists.
Primary reading
- Structure and Interpretation of Computer Programs: University of California, Berkeley
- The Python Tutorial: Python Software Foundation