Object-Oriented Design
CSC108H1 showed you how to write a class. This unit is about designing one: what the attributes mean, what must always be true of them, and how to state that so the invariant survives every method you add.
- Write a representation invariant and check methods preserve it.
- Document attributes with types and meanings.
- Decide what belongs in the public interface.
- Implement
__eq__,__str__and__len__where they earn their place.
A class is a promise that its data stays in a sensible state. Writing that promise down turns a vague intention into something you can check every method against — and most bugs in this course are a method quietly breaking one.
A bank account whose balance and transaction history can disagree is a serious bug. Stating "balance equals the sum of the transactions" as an invariant makes the requirement explicit and testable rather than assumed.
Design by contract came from Bertrand Meyer's Eiffel language in the 1980s, where preconditions, postconditions and invariants were checked by the compiler. Python leaves them as documentation, so the discipline has to be yours.
Documenting the representation
class Course:
"""A course with a fixed capacity and a list of enrolled students.
Attributes:
code: the course code, such as 'CSC148H1'
capacity: the maximum number of students, a positive int
students: the names of enrolled students
Representation invariants:
- len(self.students) <= self.capacity
- there are no duplicates in self.students
- self.capacity > 0
"""
code: str
capacity: int
students: list
def __init__(self, code: str, capacity: int) -> None:
"""Initialize an empty course with the given code and capacity.
Preconditions: capacity > 0
"""
self.code = code
self.capacity = capacity
self.students = []If the invariant holds when a method starts, it must hold when the method ends. That is the obligation, and it is what makes the class trustworthy from the outside.
def enrol(self, name: str) -> bool:
"""Add name to this course and return whether it succeeded.
Fails if the course is full or name is already enrolled.
>>> c = Course('CSC148H1', 2)
>>> c.enrol('Ada')
True
>>> c.enrol('Ada')
False
"""
if len(self.students) >= self.capacity:
return False
if name in self.students:
return False
self.students.append(name)
return TruePublic interface
Ask what callers need to do, not what they need to see. A method can enforce the invariant; a bare attribute cannot.
c = Course('CSC148H1', 2)
c.students.append('Ada')
c.students.append('Ada') # duplicate - invariant broken
c.students.append('Alan') # over capacity - invariant brokenstudents is a public list, a client
can bypass enrol entirely and leave the object in a state the
class claims is impossible._students and exposing a
class_list() method that returns a copy closes the
hole. Returning the list itself would reopen it, since the caller could
mutate the copy's original.def class_list(self) -> list:
"""Return a new list of the names enrolled in this course.
Mutating the returned list does not affect this course.
"""
return self._students[:]The docstring states the guarantee. Without it, a caller cannot tell whether mutating the result is safe.
Special methods worth defining
__str__ — always. Debugging without it
means reading memory addresses.__eq__ — when two distinct objects can
be meaningfully equal.__len__ — when the object has an obvious
size.__contains__ — when membership makes
sense, enabling in.def __len__(self) -> int:
"""Return the number of students enrolled in this course.
>>> c = Course('CSC148H1', 2)
>>> c.enrol('Ada')
True
>>> len(c)
1
"""
return len(self._students)
def __contains__(self, name: str) -> bool:
"""Return whether name is enrolled in this course.
>>> c = Course('CSC148H1', 2)
>>> c.enrol('Ada')
True
>>> 'Ada' in c
True
"""
return name in self._studentsThese let client code read naturally — len(c) and
'Ada' in c — while keeping the list
private.
Defining __eq__ without __hash__ makes instances
unhashable, so they can no longer go in a set or serve as dictionary keys.
Python does this deliberately: two objects that compare equal must hash
equally, and it will not guess how.
• Stating an invariant and then writing a method that breaks
it.
• Exposing a mutable attribute and expecting clients not to touch
it.
• Returning an internal list directly from a getter.
• Documenting attribute types but not their meanings. "students: list"
says nothing about what is in it.
• Defining __eq__ and then wondering why instances cannot go
in a set.
Practice
Write out each answer before opening it. Original exercises written for this guide in the style of the course's weekly work.
Write a representation invariant for a Rectangle class
with width and height.
Answer
self.width > 0
and self.height > 0. Whether zero is allowed is a design
decision, but it must be stated either way.A Playlist has invariant "no duplicate songs". Which method
is most likely to violate it?
Answer
Why return self._items[:] rather than
self._items?
Answer
Should two Course objects with the same code be
equal?
Answer
Design a BankAccount with a balance that cannot go
negative.
Solution
class BankAccount:
"""An account with a non-negative balance.
Attributes:
owner: the account holder's name
Representation invariants:
- self._balance >= 0
"""
def __init__(self, owner: str) -> None:
"""Initialize an account for owner with a zero balance."""
self.owner = owner
self._balance = 0
def withdraw(self, amount: int) -> bool:
"""Withdraw amount and return whether it succeeded.
Fails if amount exceeds the balance.
Preconditions: amount > 0
"""
if amount > self._balance:
return False
self._balance = self._balance - amount
return TrueWhich special methods suit a Deck of cards?
Answer
__len__ for
the count, __contains__ for whether a card is present,
__str__ for display. __eq__ is doubtful — two
decks with the same cards in a different order probably are not the same
deck.You define __eq__ and then cannot put instances in a set.
Why?
Answer
__eq__ sets __hash__ to None. Objects
that compare equal must hash equally, and Python refuses to assume how.
Define __hash__ yourself if the objects are
immutable.A class stores a sorted list. Should sort() be
public?
Answer
sort() implies the list might be
unsorted, which contradicts the invariant.Expanded topic checklist
Course coverage. This unit is part of the complete CSC148H1 topic map. It should cover the following items before you move on:
- Class documentation. Purpose, attributes with meanings, representation invariants.
- Invariant preservation. Checking every method against the stated conditions.
- Encapsulation. Private attributes, safe getters, and why exposing mutable state leaks the invariant.
- Special methods. Which to define and the hashability
consequence of
__eq__.
The calendar names "encapsulation and information-hiding" and "object-oriented programming". Representation invariants are marked on assignments; check Quercus for the exact required format.
The invariant defines what a valid object is. Encapsulation protects it from outside interference. Methods are obliged to preserve it. Every class in the rest of the course is written this way.
Deep practice extensions
Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.
What is a representation invariant?
Guide
What does every method owe the invariant?
Guide
Why is returning an internal list dangerous?
Guide
What does defining __eq__ break?
Guide
__hash__.