GoF patterns applied to the bookshop: Factory Method, Singleton, Observer, Strategy, and Decorator — each with SysML v2 mapping.
Module 6: Interfaces & Contracts
Defining formal contracts that classes must fulfil — ABCs, Protocol, Design by Contract, and SysML v2 interface def.
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.
| Mechanism | Subtyping model | How it works |
|---|---|---|
| ABC | Nominal | Class must explicitly inherit from the ABC and implement all abstract methods. Python raises TypeError if any are missing. |
| Protocol | Structural | Any class with the required methods is compatible, regardless of inheritance. No explicit declaration needed. |
interface keyword. ABCs (abc.ABC) provide nominal subtyping; typing.Protocol provides structural subtyping (duck typing with type-checker support).ABCs as Interfaces
Three focused interfaces for the bookshop domain, each with a single responsibility:
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:
# 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 TrueTypeError at instantiation time — not at call time. You cannot create an incomplete instance.Protocol: Structural Subtyping
Any class that has the right methods satisfies a Protocol — no inheritance required. Ideal for third-party types and test doubles:
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)) # TrueDesign by Contract
Design by Contract formalises preconditions (what a method expects), postconditions (what it guarantees), and invariants (what always holds):
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 = valueif/raise at the entry. Postconditions: checked with assert at the exit (disable with -O in production). Invariants: checked in property getters/setters.SysML v2 Bridge
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;
}interface def with abstract actionsclass X(InterfaceA, InterfaceB)part def X :> InterfaceA, InterfaceBrequire constraint in action defensure constraint in action defSummary
Separate specification (what) from implementation (how). Code depends on the interface, not the concrete class.
Explicit inheritance required. Python enforces completeness at instantiation. Best for controlled codebases.
Any class with the right methods qualifies. No inheritance needed. Best for external types and mocks.
class X(ABC_A, ABC_B) enables ISP — small focused contracts over one large interface.
Preconditions (if/raise), postconditions (assert), invariants make contracts explicit and checkable.
interface def + part def X :> Interface maps directly to Python ABCs.