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

Module 6: Interfaces & Contracts

Defining formal contracts that classes must fulfil — ABCs, Protocol, Design by Contract, and SysML v2 interface def.

Prereq: OOD Module 5Python 3UML 2.5SysML v2
1

Why Interfaces?

An interface defines what a class can do without specifying how. Code that depends on an interface is decoupled from any particular implementation — you can swap one implementation for another without changing the caller.

MechanismSubtyping modelHow it works
ABCNominalClass must explicitly inherit from the ABC and implement all abstract methods. Python raises TypeError if any are missing.
ProtocolStructuralAny class with the required methods is compatible, regardless of inheritance. No explicit declaration needed.
ℹ️ PYTHON VS JAVA
Python has no interface keyword. ABCs (abc.ABC) provide nominal subtyping; typing.Protocol provides structural subtyping (duck typing with type-checker support).
2

ABCs as Interfaces

Three focused interfaces for the bookshop domain, each with a single responsibility:

Python
from abc import ABC, abstractmethod
from typing import TypeVar, Type
T = TypeVar("T")

class Payable(ABC):
    """Anything that can be paid for and refunded."""
    @abstractmethod
    def process(self) -> bool: ...
    @abstractmethod
    def refund(self) -> bool: ...

class Discountable(ABC):
    """Anything that can have a discount applied."""
    @abstractmethod
    def apply_discount(self, percentage: float) -> None: ...
    @abstractmethod
    def get_discount_rate(self) -> float: ...

class Serialisable(ABC):
    """Anything that can be serialised to/from a dict."""
    @abstractmethod
    def to_dict(self) -> dict: ...
    @classmethod
    @abstractmethod
    def from_dict(cls: Type[T], data: dict) -> T: ...

A class can implement multiple ABCs through Python's multiple inheritance:

Python
# Book implements Discountable AND Serialisable simultaneously
class Book(ABC, Discountable, Serialisable):
    def apply_discount(self, percentage: float) -> None:
        if not 0 <= percentage <= 100:
            raise ValueError(f"Discount must be 0-100, got {percentage}")
        self._price = round(self._price * (1 - percentage/100), 2)
        self._discount = percentage

    def get_discount_rate(self) -> float: return self._discount

    def to_dict(self) -> dict:
        return {"isbn":self.isbn,"title":self.title,
                "author":self.author,"price":self._price}

# Payment implements Payable
class Payment(Payable):
    def process(self) -> bool: self._status="complete"; return True
    def refund(self) -> bool:
        if self._status!="complete":
            raise RuntimeError("Cannot refund: not complete")
        self._status="refunded"; return True
diagram
Figure 1 — Payable is implemented by Payment. Discountable and Serialisable are both implemented by Book. Python allows a single class to implement multiple ABCs simultaneously.
ℹ️ ENFORCEMENT
If a subclass omits an abstract method, Python raises TypeError at instantiation time — not at call time. You cannot create an incomplete instance.
3

Protocol: Structural Subtyping

Any class that has the right methods satisfies a Protocol — no inheritance required. Ideal for third-party types and test doubles:

Python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Priceable(Protocol):
    """Anything with a price that can be discounted."""
    @property
    def price(self) -> float: ...
    def apply_discount(self, percentage: float) -> None: ...

# GiftCard satisfies Priceable WITHOUT inheriting from it
class GiftCard:
    def __init__(self, code: str, value: float):
        self.code=code; self._price=value

    @property
    def price(self) -> float: return self._price

    def apply_discount(self, percentage: float) -> None:
        self._price = round(self._price * (1 - percentage/100), 2)

def apply_sale_price(item: Priceable, pct: float) -> None:
    item.apply_discount(pct)
    print(f"  New price: £{item.price:.2f}")

book = DigitalBook("978-1-11","Clean Code","Martin",29.99,"PDF",5.2,"...")
card = GiftCard("GC-001", 25.00)
apply_sale_price(book, 10)   # ok — Book has price + apply_discount
apply_sale_price(card, 10)   # ok — GiftCard satisfies Priceable structurally
print(isinstance(card, Priceable))  # True
diagram
Figure 2 — Both Book (which inherits Discountable) and GiftCard (which inherits nothing) satisfy the Priceable Protocol because they have the required methods.
💡 WHEN TO USE WHICH
Use ABC for strict enforcement within a controlled codebase. Use Protocol for flexible structural typing — especially for test mocks and external/third-party types you cannot change.
4

Design by Contract

Design by Contract formalises preconditions (what a method expects), postconditions (what it guarantees), and invariants (what always holds):

Python
class Book(ABC, Discountable, Serialisable):
    """
    Invariants:
      - isbn is a non-empty string
      - _price >= 0
      - 0 <= _discount <= 100
    """
    def apply_discount(self, percentage: float) -> None:
        """
        Pre:  0 <= percentage <= 100
        Post: self.price == old_price * (1 - percentage/100)
        """
        if not 0 <= percentage <= 100:
            raise ValueError(f"Discount must be 0-100, got {percentage}")
        old_price = self._price
        self._discount = percentage
        self._price = round(old_price * (1 - percentage/100), 2)
        # Postcondition check (can disable with -O in prod)
        assert self._price >= 0, "Price invariant violated"

    @price.setter
    def price(self, value: float) -> None:
        if value < 0: raise ValueError("Price cannot be negative")
        self._price = value
ℹ️ CONTRACT LAYERS
Preconditions: checked with if/raise at the entry. Postconditions: checked with assert at the exit (disable with -O in production). Invariants: checked in property getters/setters.
5

SysML v2 Bridge

SysML v2
interface def Payable {
    action def process : Boolean;
    action def refund  : Boolean;
}
interface def Discountable {
    action def apply_discount { in percentage : Real; }
    action def get_discount_rate : Real;
}
interface def Serialisable {
    action def to_dict   : Dictionary;
    action def from_dict { in data : Dictionary; }
}
// Book implements multiple interfaces
abstract part def Book :> Discountable, Serialisable {
    attribute isbn  : String;
    attribute price : Real;
    abstract action def get_details : String;
}
// Payment implements Payable
part def Payment :> Payable {
    :>> process : Boolean;
    :>> refund  : Boolean;
}
Python / OOD
SysML v2
ABC with @abstractmethod
interface def with abstract actions
class X(InterfaceA, InterfaceB)
part def X :> InterfaceA, InterfaceB
Protocol (structural)
Structural conformance by method signature
Precondition (if/raise)
require constraint in action def
Postcondition (assert)
ensure constraint in action def
6

Summary

Interfaces

Separate specification (what) from implementation (how). Code depends on the interface, not the concrete class.

ABC (nominal)

Explicit inheritance required. Python enforces completeness at instantiation. Best for controlled codebases.

Protocol (structural)

Any class with the right methods qualifies. No inheritance needed. Best for external types and mocks.

Multiple interfaces

class X(ABC_A, ABC_B) enables ISP — small focused contracts over one large interface.

Design by Contract

Preconditions (if/raise), postconditions (assert), invariants make contracts explicit and checkable.

SysML v2

interface def + part def X :> Interface maps directly to Python ABCs.

📚
Next — Module 7: Design Patterns

GoF patterns applied to the bookshop: Factory Method, Singleton, Observer, Strategy, and Decorator — each with SysML v2 mapping.