Ran Wei / Object-Oriented Design / Module 3
中文
Object-Oriented Design — Ran Wei

Module 3: Inheritance & Hierarchies

Reuse and specialise behaviour through class hierarchies — extending Book into DigitalBook and PhysicalBook, and introducing abstract classes.

Prereq: OOD Module 2 Python 3 ABC super() LSP SysML v2 Bridge
1

The “Is-A” Relationship

Inheritance models an “is-a” relationship. A DigitalBook is a Book. A PremiumCustomer is a Customer. The child class inherits all attributes and methods of the parent and can add its own or override inherited ones.

RelationshipUse
Is-aInheritance. DigitalBook is-a Book — shares all Book behaviour and adds its own.
Has-aComposition. ShoppingCart has Books — it owns references to Book objects but is not itself a Book.
ℹ️ The Golden Rule
Only use inheritance when the subclass truly is a specialisation of the superclass — when it can always be used wherever the superclass is expected (Liskov Substitution Principle). If you find yourself overriding methods to do nothing or raise errors, composition is almost certainly the right choice.
2

Single Inheritance

Our bookshop sells two kinds of book: digital downloads and physical copies. Both share core Book attributes, but each has its own additional properties. We first make Book abstract so it cannot be instantiated directly:

Python
from abc import ABC, abstractmethod

class Book(ABC):
    """Abstract base class for all book types."""

    def __init__(self, isbn, title, author, price, stock=0):
        if not isbn.strip(): raise ValueError("ISBN cannot be blank")
        self.isbn = isbn; self.title = title; self.author = author
        self._price = 0.0; self._stock = 0
        self.price = price; self.stock = stock

    @abstractmethod
    def get_details(self) -> str:
        """Every subclass must provide its own description."""
        ...

    def is_available(self): return self.stock > 0
    def apply_discount(self, percent):
        if not 0 <= percent <= 100: raise ValueError("Discount must be 0–100")
        self.price *= (1 - percent / 100)


class DigitalBook(Book):
    """A downloadable e-book."""
    def __init__(self, isbn, title, author, price,
                 file_format, file_size, download_url):
        super().__init__(isbn, title, author, price, stock=999)
        self.file_format  = file_format
        self.file_size    = file_size
        self.download_url = download_url

    def get_details(self) -> str:
        return (f"{self.title} by {self.author} | {self.file_format} "
                f"({self.file_size:.1f} MB) £{self.price:.2f}")

    def download(self): print(f"Downloading from {self.download_url}")


class PhysicalBook(Book):
    """A printed book stored in a warehouse."""
    SHIPPING_RATE = 0.005  # £ per gram

    def __init__(self, isbn, title, author, price, stock,
                 weight, dimensions, warehouse):
        super().__init__(isbn, title, author, price, stock)
        self.weight = weight; self.dimensions = dimensions
        self.warehouse = warehouse

    def get_details(self) -> str:
        return (f"{self.title} by {self.author} | Paperback "
                f"£{self.price:.2f} ({self.weight}g)")

    def get_shipping_cost(self) -> float:
        return round(self.weight * self.SHIPPING_RATE, 2)
UML class hierarchy: Book, DigitalBook, PhysicalBook
Figure 1 — UML class hierarchy: abstract Book with two concrete subclasses. The hollow arrowhead denotes generalisation (inheritance). «abstract» marks the base class and the abstract method.
3

Constructor Chaining with super()

When a subclass defines __init__, it must call super().__init__() to ensure the parent class is properly initialised. Without it, all the Book invariants and property setup are skipped:

Python
digital = DigitalBook(
    isbn="978-0-13-468599-1", title="The Pragmatic Programmer",
    author="Hunt & Thomas", price=34.99,
    file_format="EPUB", file_size=8.4,
    download_url="https://books.example.com/pp.epub"
)

print(digital.isbn)           # "978-0-13-468599-1"
print(digital.is_available()) # True  (stock=999)
print(digital.get_details())  # "The Pragmatic Programmer | EPUB (8.4 MB) £34.99"
digital.download()             # "Downloading from https://..."
digital.apply_discount(10)
print(digital.price)          # 31.491
ℹ️ super() and MRO
super() does not mean “call the parent class directly.” It calls the next class in the Method Resolution Order (MRO) — the linearised list Python uses to resolve method lookups. For simple single-inheritance chains this is always the direct parent, but the distinction matters for multiple inheritance.
4

Method Overriding

Both DigitalBook and PhysicalBook override get_details(). The method name is the same; the implementation differs. Python selects the correct version at runtime based on the actual type of the object:

Python
books = [
    DigitalBook("978-1-11", "Clean Code", "Martin", 29.99, "PDF", 5.2, "..."),
    PhysicalBook("978-1-22", "Refactoring", "Fowler", 39.99, 5, 520, "...", "WH1"),
]

for book in books:
    print(book.get_details())   # correct version called automatically

# Clean Code by Martin | PDF (5.2 MB) £29.99
# Refactoring by Fowler | Paperback £39.99 (520g)

This is polymorphism — the same method call produces different behaviour depending on the object's type. Module 4 covers polymorphism in depth.

⚠️ Pitfall — Violating LSP
An override that changes the method's contract — requiring different arguments, returning a different type, or raising exceptions the parent does not — violates the Liskov Substitution Principle. Code that works with a Book must work equally with a DigitalBook or PhysicalBook.
5

Abstract Classes and @abstractmethod

An abstract class cannot be instantiated directly — it exists only to be subclassed. @abstractmethod forces every concrete subclass to provide its own implementation:

Python
# Cannot instantiate the abstract class:
book = Book("978-0", "Test", "Author", 9.99)
# TypeError: Can't instantiate abstract class Book
#            with abstract method get_details

# A subclass that forgets get_details also fails:
class BrokenBook(Book): pass

broken = BrokenBook("978-0", "Test", "Author", 9.99)
# TypeError: Can't instantiate abstract class BrokenBook
💡 Design Intent
@abstractmethod is a contract: “Every concrete Book must know how to describe itself.” The abstract class defines the interface; each subclass provides the implementation. This is the foundation of polymorphism.

PremiumCustomer

The same pattern applied to Customer:

Python
class PremiumCustomer(Customer):
    """A customer enrolled in the loyalty programme."""

    def __init__(self, customer_id, name, email, discount_rate=0.10):
        super().__init__(customer_id, name, email)
        if not 0 <= discount_rate <= 1:
            raise ValueError("Discount rate must be between 0 and 1")
        self._loyalty_points = 0
        self._discount_rate  = discount_rate

    def get_discount_rate(self): return self._discount_rate

    def add_points(self, points: int):
        if points <= 0: raise ValueError("Points must be positive")
        self._loyalty_points += points

    def get_details(self) -> str:
        return (f"{self.get_name()} (Premium) | "
                f"Points: {self._loyalty_points} | "
                f"Discount: {self._discount_rate*100:.0f}%")
UML: Customer and PremiumCustomer
Figure 2Customer and PremiumCustomer. The hollow arrowhead denotes inheritance. PremiumCustomer inherits all Customer attributes and methods, then adds loyalty-programme behaviour.
6

Composition vs Inheritance

Inheritance is powerful but often overused. A useful heuristic: prefer composition over inheritance unless the is-a test is clearly satisfied and unlikely to change.

FactorFavour InheritanceFavour Composition
Relationship“Is-a”: DigitalBook is-a Book“Has-a”: ShoppingCart has Books
Reuse scopeAll instances need the parent's behaviourOnly some behaviours need sharing
FlexibilityHierarchy is stable, unlikely to changeBehaviour may vary or reconfigure at runtime
CouplingTight coupling acceptable — subclass knows parent wellLooser coupling preferred — components are independent
ℹ️ Practical Rule
If the hierarchy grows beyond 2–3 levels, or you need to inherit from multiple unrelated classes, stop and consider composition. Deep hierarchies are fragile — a change to a base class ripples through all descendants.
7

SysML v2 Bridge

SysML v2 provides direct language constructs for inheritance. The mapping is precise:

Python / OODSysML v2
class Child(Parent)part def Child :> Parent { ... }
super().__init__()Implicit — inherited features available automatically
Override method:>> methodName { ... }
Abstract class (ABC)abstract part def Book { ... }
@abstractmethodabstract action def get_details : String;
SysML v2
abstract part def Book {
    attribute isbn   : String;
    attribute title  : String;
    attribute author : String;
    private attribute _price : Real := 0.0;

    abstract action def get_details : String;  // must be redefined
    action def is_available  : Boolean;
    action def apply_discount { in percent : Real; }
}

part def DigitalBook :> Book {
    attribute file_format   : String;
    attribute file_size     : Real;
    attribute download_url  : String;

    :>> stock = 999;              // redefine with fixed value
    :>> get_details : String;     // override the abstract action
    action def download : Void;   // new capability
}

part def PhysicalBook :> Book {
    attribute weight     : Real;
    attribute dimensions : String;
    attribute warehouse  : String;

    :>> get_details : String;
    action def get_shipping_cost : Real;
}
Python / OOD
SysML v2
class Child(Parent)
part def Child :> Parent
Override method
:>> methodName
Abstract class
abstract part def
@abstractmethod
abstract action def
💡 :> vs :>>
:> (subclassification) declares “this part def specialises that one” — equivalent to class inheritance. :>> (redefinition) overrides a specific feature from the parent — equivalent to method overriding. Both are needed together: :> establishes the hierarchy, :>> customises individual features within it.
8

Summary

Is-A

Use inheritance only when the subclass truly is a specialisation of the superclass and satisfies the Liskov Substitution Principle.

super()

Always call super().__init__() in a subclass constructor to ensure the parent is fully initialised before adding subclass state.

Method Overriding

Each subclass provides its own implementation. Python selects the correct version at runtime. The contract must be preserved.

Abstract Classes

ABC + @abstractmethod define an interface that all concrete subclasses must implement. Cannot be instantiated directly.

Composition

Prefer composition over inheritance when the is-a relationship is unclear, the hierarchy would grow deep, or runtime flexibility is needed.

SysML v2

:> declares subclassification; :>> redefines a feature. abstract part def and abstract action def enforce the abstract class pattern.

🧬
Next — Module 4

Polymorphism

How the same method call produces different behaviour, duck typing in Python, operator overloading, and the full polymorphism story in UML and SysML v2.