You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
This lesson covers abstract classes and interfaces — the two mechanisms OOP provides for realising abstraction, the fourth pillar. Both enforce structure across a class hierarchy by specifying what must be done while leaving how it is done to concrete subclasses. They are central to designing large, maintainable and testable software, and they explain how strongly typed languages obtain the benefits of multiple inheritance safely.
This lesson addresses abstraction, abstract classes/methods and interfaces within the object-oriented programming paradigm of the AQA A-Level Computer Science (7517) specification (subject content area 4.1.2). It covers: abstraction as the hiding of implementation behind an essential interface; abstract classes that cannot be instantiated and may declare abstract methods; the requirement that concrete subclasses implement every abstract method; interfaces as pure contracts; the comparison of abstract classes with interfaces; the design benefits (loose coupling, testability, design by contract); and the choice between inheritance ("is-a") and composition ("has-a"). It builds directly on inheritance and polymorphism from the previous lesson.
Abstraction is the principle of exposing only the essential features of something while hiding the implementation detail. It lets a programmer reason in high-level terms without being overwhelmed by internals. You drive a car through the steering wheel, pedals and gear selector (its interface) without understanding the combustion or electrical systems beneath (its implementation). If the engine is replaced with an electric motor, the driving interface is unchanged.
In OOP, abstraction is delivered through two related constructs:
Both say to a subclass: "you must provide these operations", while remaining silent (or partial) on the detail.
An abstract class has three defining characteristics:
An abstract class is therefore a partial template: it captures what every subclass shares (concrete members) and mandates what each must supply itself (abstract members).
# AQA-style OOP pseudocode
ABSTRACT CLASS Shape
PRIVATE colour: STRING
PUBLIC PROCEDURE new(c: STRING)
colour = c
END PROCEDURE
PUBLIC FUNCTION getColour() RETURNS STRING
RETURN colour
END FUNCTION
ABSTRACT FUNCTION area() RETURNS REAL // no body — subclass must supply
ABSTRACT FUNCTION perimeter() RETURNS REAL // no body — subclass must supply
PUBLIC FUNCTION describe() RETURNS STRING // concrete — inherited by all
RETURN "A " + colour + " shape with area " + STR(area())
END FUNCTION
END CLASS
CLASS Circle INHERITS Shape
PRIVATE radius: REAL
PUBLIC PROCEDURE new(c: STRING, r: REAL)
SUPER.new(c)
radius = r
END PROCEDURE
PUBLIC FUNCTION area() RETURNS REAL
RETURN 3.14159 * radius * radius
END FUNCTION
PUBLIC FUNCTION perimeter() RETURNS REAL
RETURN 2 * 3.14159 * radius
END FUNCTION
END CLASS
describe() is concrete, yet it calls the abstract area(). This is polymorphism again: every Circle, Rectangle or Triangle inherits the same describe(), which automatically uses each subclass's own area().
Python implements abstract classes through the abc module: a class inheriting ABC with at least one @abstractmethod cannot be instantiated.
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, colour: str):
self._colour = colour
def get_colour(self) -> str:
return self._colour
@abstractmethod
def area(self) -> float:
... # no implementation
@abstractmethod
def perimeter(self) -> float:
...
def describe(self) -> str: # concrete, inherited unchanged
return f"A {self._colour} shape with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, colour: str, radius: float):
super().__init__(colour)
self.__radius = radius
def area(self) -> float:
return 3.14159 * self.__radius ** 2
def perimeter(self) -> float:
return 2 * 3.14159 * self.__radius
# shape = Shape("red") # TypeError: can't instantiate abstract class Shape
circle = Circle("blue", 5)
print(circle.describe()) # A blue shape with area 78.54
If a subclass forgets to implement any abstract method, it too remains abstract and cannot be instantiated — the compiler/interpreter enforces the contract for you, turning a potential run-time bug into an early, obvious error.
| Use case | Explanation |
|---|---|
| Shared implementation | Subclasses share substantial common code (held in concrete methods) but differ in a few operations. |
| Enforcing structure | You must guarantee that every subclass provides certain operations. |
| Partial abstraction | Some behaviour has a sensible default; the rest must be supplied per subclass. |
Exam Tip: If asked to distinguish an abstract class from an ordinary class, give the three discriminators: (1) it cannot be instantiated; (2) it may contain abstract methods with no body; (3) concrete subclasses must implement every abstract method or remain abstract themselves.
An interface specifies a contract — a set of method signatures that any class claiming to implement it must provide. Traditionally an interface contains no implementation and no attributes (modern languages relax this with default methods and constants). An interface answers "what can this object do?" without saying anything about how or about what the object is.
| Feature | Abstract class | Interface |
|---|---|---|
| Instantiation | Cannot be instantiated | Cannot be instantiated |
| Concrete methods | Yes | Traditionally none (modern languages allow defaults) |
| Attributes / state | Yes | Traditionally none |
| A class may have | One abstract superclass | Many interfaces |
| Models | "is-a" (a Circle is a Shape) | "can-do" / a role (a Sprite can be Drawable) |
| Primary purpose | Share code + enforce structure | Pure capability contract |
The decisive difference for design: a class can implement many interfaces but typically extend only one (abstract) class. Interfaces therefore give Java and C# — which forbid multiple class inheritance — a safe way to let one class fulfil several roles.
# AQA-style OOP pseudocode
INTERFACE Drawable
PROCEDURE draw()
FUNCTION getPosition() RETURNS POSITION
END INTERFACE
INTERFACE Resizable
PROCEDURE resize(factor: REAL)
END INTERFACE
CLASS GameSprite IMPLEMENTS Drawable, Resizable
PRIVATE x: INTEGER
PRIVATE y: INTEGER
PRIVATE size: REAL
PUBLIC PROCEDURE draw()
OUTPUT "Drawing sprite at (" + STR(x) + ", " + STR(y) + ")"
END PROCEDURE
PUBLIC FUNCTION getPosition() RETURNS POSITION
RETURN (x, y)
END FUNCTION
PUBLIC PROCEDURE resize(factor: REAL)
size = size * factor
END PROCEDURE
END CLASS
GameSprite implements two interfaces, so it can be passed both to code that expects something Drawable and to code that expects something Resizable.
Python has no interface keyword; an abstract class whose methods are all abstract serves the same role, and a class may inherit several such classes:
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self) -> None: ...
@abstractmethod
def get_position(self) -> tuple: ...
class Resizable(ABC):
@abstractmethod
def resize(self, factor: float) -> None: ...
class GameSprite(Drawable, Resizable): # implements BOTH contracts
def __init__(self, x: int, y: int, size: float):
self.__x, self.__y, self.__size = x, y, size
def draw(self) -> None:
print(f"Drawing sprite at ({self.__x}, {self.__y})")
def get_position(self) -> tuple:
return (self.__x, self.__y)
def resize(self, factor: float) -> None:
self.__size *= factor
classDiagram
class Drawable {
<<interface>>
+draw() void
+getPosition() Position
}
class Resizable {
<<interface>>
+resize(factor) void
}
Drawable <|.. GameSprite
Resizable <|.. GameSprite
class GameSprite {
-x: Integer
-y: Integer
-size: Real
+draw() void
+getPosition() Position
+resize(factor) void
}
The dashed arrows (<|..) are UML notation for "implements an interface", distinct from the solid <|-- used for class inheritance.
save(target: Storage) routine works with a FileStorage, a CloudStorage or a MockStorage without change.A UK 13-amp socket is an interface. Any appliance with a conforming plug — lamp, kettle or charger — can be connected. The socket neither knows nor cares what the appliance does internally; it only guarantees the agreed "shape" of the connection. Swapping the lamp for a kettle requires no change to the wiring.
Inheritance models "is-a"; composition models "has-a", building an object from references to other objects. Composition is frequently preferred because it couples classes more loosely and avoids brittle, deep hierarchies.
| Approach | Relationship | Example |
|---|---|---|
| Inheritance | "is-a" | A Circle is a Shape |
| Composition | "has-a" | A Car has an Engine |
class Engine:
def __init__(self, horsepower: int):
self.__horsepower = horsepower
def start(self) -> None:
print(f"Engine with {self.__horsepower}HP started")
class Car:
def __init__(self, make: str, engine: Engine):
self.__make = make
self.__engine = engine # composition: a Car HAS an Engine
def start(self) -> None:
print(f"Starting {self.__make}")
self.__engine.start() # delegate to the contained object
Because the Engine is passed in, you can give a Car a PetrolEngine, a DieselEngine or an ElectricMotor (all implementing a common Engine interface) at construction time — far more flexible than baking the engine type into a subclass.
Exam Tip: A frequent design question asks which relationship fits a scenario. Apply the language test: if "X is a Y" reads naturally, use inheritance; if "X has a Y", use composition. "A SavingsAccount is a BankAccount" → inheritance; "A BankAccount has a TransactionLog" → composition.
A natural question is: if multiple inheritance causes the diamond problem, how do Java and C# let a class take on several roles? The answer is interfaces. A class may inherit from only one (abstract) class but may implement many interfaces. Because a traditional interface carries no implementation and no state, there is nothing to conflict — if two interfaces both declare a save() method, the implementing class simply provides one save() that satisfies both. The ambiguity of "which inherited body do I run?" never arises, because interfaces contribute no bodies.
This is a deliberate language-design trade-off:
| Language | Class inheritance | Interface implementation | Diamond risk |
|---|---|---|---|
| Java / C# | Single only | Multiple allowed | Avoided — interfaces are stateless |
| C++ | Multiple allowed | (uses multiple inheritance) | Present — handled by explicit rules |
| Python | Multiple allowed | (uses ABCs) | Resolved by the Method Resolution Order |
So an interface gives you the useful half of multiple inheritance — the ability to fulfil several contracts — while discarding the dangerous half — inheriting conflicting implementations. This is one of the strongest justifications for the interface concept and a high-value exam point.
It is worth appreciating why the diamond problem is genuinely hard and not merely a technicality. If class D inherits a concrete save() from both B and C (which each inherited and overrode it from a common A), the language must answer: when D calls save(), whose version runs? Any automatic choice risks surprising the programmer, and forcing the programmer to disambiguate every such clash makes multiple inheritance cumbersome. Interfaces dissolve the dilemma at its root: with no inherited bodies, there is simply nothing to choose between. This is a recurring theme in language design — removing a whole class of bugs by removing the feature that makes them possible — and citing it shows examiners you understand not just what interfaces are but why they were invented.
One of the most important synoptic ideas in the whole course is that an abstract data type (ADT) — a stack, queue, list or priority queue from area 4.2 — is conceptually an interface. An ADT specifies the operations (the contract) without committing to how they are stored. A stack promises push, pop, peek and is_empty; whether those are implemented over a fixed array or a linked list is hidden behind the contract.
from abc import ABC, abstractmethod
class Stack(ABC):
"""The Stack ADT — the contract, independent of implementation."""
@abstractmethod
def push(self, item) -> None: ...
@abstractmethod
def pop(self): ...
@abstractmethod
def peek(self): ...
@abstractmethod
def is_empty(self) -> bool: ...
class ArrayStack(Stack):
"""One implementation: backed by a Python list."""
def __init__(self):
self.__items = []
def push(self, item) -> None:
self.__items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
return self.__items.pop()
def peek(self):
if self.is_empty():
raise IndexError("peek at empty stack")
return self.__items[-1]
def is_empty(self) -> bool:
return len(self.__items) == 0
Any code written against the Stack contract — for example, the call-stack model used to convert recursion to iteration — works unchanged whether it is handed an ArrayStack or a future LinkedStack. This separation of contract from implementation is the single most powerful idea abstraction gives you, and it ties this lesson directly to the data-structures unit.
It is worth being precise about the three kinds of method you have now met, because exam questions test the distinctions directly.
| Kind | Has a body? | Where it lives | Subclass obligation |
|---|---|---|---|
| Concrete method | Yes | Ordinary or abstract class | May override (optional) |
| Abstract method | No (signature only) | Abstract class / interface | Must implement |
| Interface method (traditional) | No | Interface | Must implement |
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.