CMPUT-274 · Lesson 5 of 12 · 29 min
Object-oriented programming II: inheritance, composition, interfaces and design principles
Lesson 4 introduced objects, encapsulation and polymorphism. This lesson covers the ways classes relate to one another. Inheritance lets one class extend another, reusing its code and standing in for it. Composition builds objects out of other objects. Interfaces and abstract base classes state what an object must be able to do without saying how. These tools are powerful and easy to misuse, so the lesson ends with the design principles, known by the acronym SOLID, that experienced programmers use to keep object-oriented code flexible. Software engineering at the scale of teams and large systems is the subject of CMPUT-301; here the focus is on the paradigm's building blocks.
Inheritance: extending a class
A subclass inherits the attributes and methods of its superclass and can add new ones or override inherited ones. Inheritance models an is-a relationship: a savings account is an account, so code that works with accounts should work with savings accounts. Overriding methods is how subclasses specialize behavior, and `super()` lets an overriding method call the superclass version so that the subclass extends rather than replaces it.
class Account:
def __init__(self, owner, balance=0):
self.owner, self.balance = owner, balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance -= amount
def month_end(self):
pass # nothing by default
def __repr__(self):
return f"{type(self).__name__}({self.owner}, {self.balance:.2f})"
class Savings(Account):
def __init__(self, owner, balance=0, rate=0.02):
super().__init__(owner, balance) # reuse the parent's setup
self.rate = rate
def month_end(self): # override: add interest
self.balance += self.balance * self.rate / 12
class Everyday(Account):
FEE = 4.00
def month_end(self): # override: charge a fee
self.balance -= self.FEE
accounts = [Savings("Ana", 1200), Everyday("Ben", 300), Account("Cy", 50)]
for a in accounts:
a.month_end() # dynamic dispatch picks each override
print(accounts)
print(isinstance(accounts[0], Account), Savings.__mro__[1].__name__)[Savings(Ana, 1202.00), Everyday(Ben, 296.00), Account(Cy, 50.00)] True Account
`Savings` reuses `withdraw` and `__repr__` without repeating them, and every subclass can be used wherever an `Account` is expected. The method resolution order, `__mro__`, lists the classes Python searches when looking up an attribute; for single inheritance it is simply the chain of superclasses. Python also allows multiple inheritance, with the order computed by an algorithm called C3 linearization, but most designs are clearer with single inheritance plus composition.
The Liskov substitution principle
Barbara Liskov stated the rule that makes inheritance safe: if code works correctly with objects of a superclass, it must work correctly with objects of any subclass. A subclass may accept more and promise more, but it must not require more of its callers or deliver less than the superclass promised. Violations look harmless and cause surprising bugs, because client code that is correct for the superclass breaks when a subclass object arrives.
Worked problem
The square–rectangle problem
A Rectangle class has set_width and set_height. A Square subclass overrides both so that setting either sets both, keeping the sides equal. Client code does r.set_width(4); r.set_height(5); assert r.area() == 20. What happens when r is a Square, and what does it teach?
Show solution
- For a Rectangle, width 4 and height 5 give area 20, so the assertion holds.
- For a Square, set_height(5) also sets the width to 5, so the area is 25 and the assertion fails.
- The client relied on a property the Rectangle promised: setting the height does not change the width. Square breaks that promise.
- Geometrically a square is a rectangle, but a mutable Square is not a substitutable mutable Rectangle.
Answer: The assertion fails. Is-a in the real world does not guarantee substitutability in code. Fixes include making shapes immutable, so no setters exist, or not relating Square and Rectangle by inheritance at all.
Abstract base classes and interfaces
Often a superclass exists only to define what its subclasses must do. An abstract base class declares abstract methods without implementing them, and cannot itself be instantiated. Python's `abc` module provides `ABC` and `@abstractmethod`; a subclass that does not implement every abstract method also cannot be instantiated, so the mistake is caught when the object is created rather than when the missing method is first called. Java has abstract classes and also interfaces, which declare methods and can provide default implementations but hold no instance state.
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, rows): ...
def export_all(self, tables): # template method built on export
return [self.export(rows) for rows in tables]
class CsvExporter(Exporter):
def export(self, rows):
return "\n".join(",".join(map(str, r)) for r in rows)
class BrokenExporter(Exporter): # forgot to implement export
pass
print(CsvExporter().export_all([[(1, "a"), (2, "b")]]))
for cls in (Exporter, BrokenExporter):
try:
cls()
except TypeError as err:
print(f"{cls.__name__}: {err}")['1,a\n2,b'] Exporter: Can't instantiate abstract class Exporter without an implementation for abstract method 'export' BrokenExporter: Can't instantiate abstract class BrokenExporter without an implementation for abstract method 'export'
`export_all` is a template method: a concrete method in the abstract class that calls the abstract operation, so every subclass gets it for free. This combination, an abstract class that defines the skeleton of an algorithm and subclasses that fill in steps, is one of the most common legitimate uses of inheritance.
Composition over inheritance
Composition builds an object by giving it other objects as parts and delegating work to them. It models a has-a relationship: a car has an engine. The design advice composition over inheritance, popularized by the 1994 book on design patterns by Gamma, Helm, Johnson and Vlissides, reflects hard experience. Inheritance couples a subclass tightly to its superclass's implementation, so a change in the parent can break every child, a problem called the fragile base class. Deep hierarchies also tend to explode: if notifications can be sent by email or text and can be urgent or normal, inheritance needs a class for every combination, while composition needs one object for each choice.
class Email:
def send(self, to, text): return f"email to {to}: {text}"
class Sms:
def send(self, to, text): return f"text to {to}: {text[:20]}"
class Urgent:
def format(self, text): return "URGENT " + text.upper()
class Normal:
def format(self, text): return text
class Notifier: # composed of a channel and a policy
def __init__(self, channel, policy):
self.channel, self.policy = channel, policy
def notify(self, to, text):
return self.channel.send(to, self.policy.format(text))
for ch in (Email(), Sms()):
for po in (Urgent(), Normal()):
print(Notifier(ch, po).notify("ana", "server room temperature high"))
n = Notifier(Email(), Normal())
n.policy = Urgent() # policy swapped at run time
print(n.notify("ben", "backup failed"))email to ana: URGENT SERVER ROOM TEMPERATURE HIGH email to ana: server room temperature high text to ana: URGENT SERVER ROOM T text to ana: server room temperat email to ben: URGENT BACKUP FAILED
With composition, adding a third channel, say a chat message, adds one class and immediately works with both policies. Behavior can also change at run time by replacing a part, something a fixed class hierarchy cannot do. The cost is a little more wiring: the `Notifier` must be given its parts. Use inheritance for genuine is-a relationships where substitutability holds and the superclass was designed to be extended; use composition for everything else.
| Question | Inheritance | Composition |
|---|---|---|
| Relationship modeled | is-a | has-a, uses-a |
| Coupling | Tight: subclass depends on parent internals | Loose: depends only on the part's interface |
| When behavior is fixed | At class definition time | Can change at run time by swapping parts |
| Combinations of features | One class per combination | One part per feature |
| Typical good use | Template methods, framework base classes | Strategies, policies, services, plug-ins |
The SOLID principles
Robert C. Martin collected five design principles for object-oriented code under the acronym SOLID. They are guidelines, not laws, and each can be overdone, but together they describe code that is easy to extend and hard to break. Each principle is a response to a common way object-oriented systems become rigid.
| Principle | Statement | Problem it prevents |
|---|---|---|
| Single responsibility | A class should have one reason to change | God classes that mix unrelated concerns |
| Open–closed | Open for extension, closed for modification | Editing tested code for every new feature |
| Liskov substitution | Subtypes must be usable wherever their base type is | Subclasses that break client assumptions |
| Interface segregation | Prefer small, focused interfaces to large ones | Classes forced to implement methods they do not need |
| Dependency inversion | Depend on abstractions, not concrete classes | High-level logic tied to low-level details such as a specific database |
Worked problem
Applying single responsibility and dependency inversion
A ReportGenerator class reads sales from a PostgreSQL database, computes monthly totals, formats them as HTML and emails the result. Identify the problems and redesign it.
Show solution
- It has four reasons to change: database schema, business rules, presentation and delivery. That violates single responsibility.
- The computation is tied to PostgreSQL, so it cannot be tested without a database. That violates dependency inversion.
- Split it: a SalesSource interface with a fetch method, a pure function monthly_totals(sales), a Formatter interface with HtmlFormatter, and a Sender interface with EmailSender.
- ReportGenerator receives a source, a formatter and a sender in its constructor and only coordinates them; tests pass in an in-memory source and a fake sender.
Answer: Separate the concerns into small parts behind interfaces and inject them. The business rule becomes a pure function, which also shows how functional and object-oriented ideas combine.
from typing import Protocol
class SalesSource(Protocol):
def fetch(self) -> list[tuple[str, float]]: ...
class Sender(Protocol):
def send(self, body: str) -> None: ...
def monthly_totals(sales): # pure business rule
totals = {}
for month, amount in sales:
totals[month] = totals.get(month, 0) + amount
return dict(sorted(totals.items()))
class ReportGenerator: # depends only on abstractions
def __init__(self, source: SalesSource, sender: Sender):
self.source, self.sender = source, sender
def run(self):
totals = monthly_totals(self.source.fetch())
self.sender.send("; ".join(f"{m}: {t:.2f}" for m, t in totals.items()))
class FakeSource: # test doubles, no database needed
def fetch(self): return [("2026-02", 120.0), ("2026-01", 80.5), ("2026-02", 30.0)]
class CaptureSender:
def __init__(self): self.sent = []
def send(self, body): self.sent.append(body)
cap = CaptureSender()
ReportGenerator(FakeSource(), cap).run()
print(cap.sent)['2026-01: 80.50; 2026-02: 150.00']
Inheritance and composition in Java
Java supports single inheritance of classes with `extends` and multiple inheritance of interfaces with `implements`. Since Java 17, a class or interface can be `sealed`, listing exactly which subclasses are permitted. Sealing closes a hierarchy so that the compiler knows every variant, which connects object-oriented hierarchies to the algebraic data types of lesson 3: a sealed interface with record implementations is Java's version of an enum with fields, and a switch over it can be checked for exhaustiveness.
public class Sealed {
sealed interface Payment permits Cash, Card, Transfer {}
record Cash(double amount) implements Payment {}
record Card(double amount, String last4) implements Payment {}
record Transfer(double amount, String bank) implements Payment {}
static String describe(Payment p) {
return switch (p) { // exhaustive: no default needed
case Cash c -> String.format("cash %.2f", c.amount());
case Card c -> String.format("card ending %s, %.2f", c.last4(), c.amount());
case Transfer t -> String.format("transfer from %s, %.2f", t.bank(), t.amount());
};
}
public static void main(String[] args) {
Payment[] ps = { new Cash(12.5), new Card(80, "4421"), new Transfer(250, "Northern Credit Union") };
for (Payment p : ps) System.out.println(describe(p));
}
}cash 12.50 card ending 4421, 80.00 transfer from Northern Credit Union, 250.00
Common mistakes
MistakeUsing inheritance to reuse a few methods from an unrelated class.
InsteadInherit only for genuine, substitutable is-a relationships; otherwise compose and delegate.
MistakeOverriding a method so that it requires more or promises less than the parent.
InsteadRespect the Liskov substitution principle; client code written for the parent must still work.
MistakeBuilding deep hierarchies with a class for every combination of features.
InsteadFactor independent choices into separate parts and compose them.
MistakeCreating huge interfaces that every implementer must fully support.
InsteadSplit interfaces by client need so classes implement only what they use.
MistakeHard-coding concrete dependencies such as a specific database class inside business logic.
InsteadDepend on an abstraction and inject the concrete implementation, which also makes testing easy.
Summary and key terms
- Inheritance lets a subclass reuse and override its superclass and stand in for it through dynamic dispatch.
- The Liskov substitution principle requires subclasses to honor every promise of their superclass.
- Abstract base classes and interfaces state what subclasses must implement; template methods build on abstract steps.
- Composition builds objects from parts and delegates to them, avoiding fragile base classes and combinatorial hierarchies.
- Prefer composition unless there is a true, substitutable is-a relationship.
- SOLID summarizes five principles: single responsibility, open–closed, Liskov substitution, interface segregation and dependency inversion.
- Sealed hierarchies in Java close the set of subclasses, linking object-oriented design to algebraic data types.
- inheritance
- A mechanism by which a subclass acquires the attributes and methods of a superclass.
- overriding
- Providing a subclass implementation of a method already defined in the superclass.
- Liskov substitution principle
- Objects of a subtype must be usable wherever objects of the supertype are expected without breaking correctness.
- abstract base class
- A class that declares methods subclasses must implement and that cannot itself be instantiated.
- template method
- A method defining an algorithm's skeleton that calls steps implemented by subclasses.
- composition
- Building an object from other objects that it holds and delegates to.
- delegation
- Handing a request to another object that performs the work.
- fragile base class
- The problem that changes in a superclass can break subclasses that depend on its implementation.
- dependency inversion
- Making high-level code depend on abstractions rather than on concrete low-level classes.
- sealed class
- A class or interface that restricts which classes may extend or implement it.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Write a subclass Manager of Employee that overrides pay() to add a bonus to the parent's pay.
Show answer
`class Manager(Employee):` with `def pay(self): return super().pay() + self.bonus`.
Why can a read-only Square safely be treated as a read-only Rectangle when a mutable one cannot?
Show answer
Without setters there is no promise that setting one side leaves the other unchanged, so every property the client can rely on holds for squares too.
What happens if you instantiate an ABC subclass that does not implement an abstract method?
Show answer
Python raises TypeError when the object is created.
Give an example of a has-a relationship and an is-a relationship.
Show answer
A car has an engine (composition); a truck is a vehicle (inheritance).
With 3 channels and 4 formatting policies, how many classes does each design need?
Show answer
Inheritance with one class per combination needs 12 leaf classes; composition needs 3 + 4 = 7 part classes plus one Notifier.
Which SOLID principle does a class that both parses files and draws charts violate?
Show answer
Single responsibility: it has two unrelated reasons to change.
How does dependency inversion help testing?
Show answer
Code depends on interfaces, so tests can pass in fake implementations instead of real databases or networks.
State the open–closed principle in your own words.
Show answer
You should be able to add new behavior by adding code, such as a new class, without editing existing, tested code.
What does sealed add to a Java interface?
Show answer
It restricts implementations to a listed set, so the compiler knows all variants and can check switches for exhaustiveness.
Explain the fragile base class problem with an example.
Show answer
If a superclass changes add_all to call add for each element, a subclass that overrides add to count items may double-count, breaking without any change to the subclass.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.