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 object-oriented design — how the four pillars (encapsulation, inheritance, polymorphism, abstraction) are used as design tools to model a problem domain as a hierarchy of classes. The emphasis here is the methodology: identifying classes, attributes, methods and relationships, drawing a class diagram, and judging when an OO approach is and is not appropriate. The mechanics of writing constructors, fields and overridden methods in a specific language belong to the Programming course; this lesson cross-links to that material rather than duplicating it.
This lesson develops OCR H446 section 1.2.4 (Types of Programming Language, object-oriented paradigm) at the design level. It requires you to define class, object, attribute (field), method, inheritance, encapsulation, polymorphism and abstraction; to apply those ideas by designing a class hierarchy for a described scenario; to express that design as a class diagram; and to reason about the merits and limitations of the object-oriented paradigm relative to procedural design. The companion Programming course owns the code-level realisation (constructor syntax, getter/setter implementation, calling an overridden method in a chosen language); here we own the modelling decisions and the justification behind them.
| Concept | Definition |
|---|---|
| Class | A blueprint or template that defines the attributes (data, also called fields) and methods (behaviours) that objects of that type will have |
| Object | A specific instance of a class, with its own values for the class's attributes |
| Attribute / field | A named piece of state belonging to an object |
| Method | A named behaviour (operation) an object can perform, often acting on its own attributes |
| Instantiation | The act of creating an object from a class |
Analogy: a class is like a cookie cutter (the template); objects are the individual cookies, each with its own specific details but all the same shape.
When you design a class you are answering two questions: what does an object of this type know? (its attributes) and what can it do? (its methods). Consider a Dog:
classDiagram
class Dog {
-name : String
-breed : String
-age : int
+bark() void
+fetch() void
+birthday() void
}
The minus signs mark private attributes; the plus signs mark public methods. That single design decision — data private, behaviour public — is the visible face of encapsulation, discussed below. Two object instances created from this class share the same methods but hold their own state:
| Object | name | breed | age |
|---|---|---|---|
| Object 1 | "Rex" | "Labrador" | 5 |
| Object 2 | "Bella" | "Poodle" | 3 |
The design lives in the class; the data lives in the objects. Holding that distinction firmly is the foundation of every OO design question.
Encapsulation means bundling the data (attributes) and the methods that operate on it together inside a class, and restricting direct access to the internal state from outside. As a design decision it means: expose behaviour, hide data.
| Mechanism | Detail |
|---|---|
| Private attributes | Declared private — cannot be read or written directly from outside the class |
| Public methods (getters/setters) | Controlled access provided through public accessor methods |
| Information hiding | The internal representation is hidden behind the public interface |
The design payoff is that a setter can validate. The Programming course covers the full language syntax; the design point is what the setter should enforce:
class BankAccount:
def __init__(self):
self.__balance = 0 # __ marks the attribute private
def deposit(self, amount):
if amount > 0:
self.__balance += amount
# else: reject — protects the invariant that balance
# only ever changes by a validated amount
def get_balance(self):
return self.__balance
Because __balance is private, no outside code can write account.__balance = -500; every change must pass through deposit (or a withdraw method), so the class can guarantee its own invariant (here, that the balance is only ever altered by validated amounts). That guarantee is the real design benefit.
| Benefit | Explanation |
|---|---|
| Data protection | External code cannot set invalid values directly (e.g. a negative balance) |
| Controlled access | Setter methods enforce validation rules in one place |
| Reduced coupling | Callers depend on the public interface, not the internal representation, so internals can change without breaking them |
| Easier maintenance | Changes to the internal representation are contained within the class |
| Modifier | Access |
|---|---|
| Public (+) | Accessible from anywhere |
| Private (−) | Accessible only within the class itself |
| Protected (#) | Accessible within the class and its subclasses |
Inheritance lets a new class (the subclass / child) acquire the attributes and methods of an existing class (the superclass / parent), then extend or override them. As a design tool it is how you capture an IS-A relationship and eliminate duplication across similar classes.
classDiagram
Animal <|-- Dog
Animal <|-- Cat
Dog <|-- Labrador
| Term | Definition |
|---|---|
| Superclass (parent) | The class being inherited from; holds the common attributes/methods |
| Subclass (child) | The class that inherits, then specialises |
| IS-A relationship | A subclass IS-A type of its superclass (a Dog IS-A Animal) |
The design discipline is the IS-A test: only use inheritance when the subclass genuinely is a kind of the superclass. "A Dog is an Animal" passes. "A Car is an Engine" fails — a car has an engine, which is composition, not inheritance. Misusing inheritance for HAS-A relationships is one of the most common design errors.
| Benefit | Explanation |
|---|---|
| Code reuse | Common members are defined once in the superclass and shared by every subclass |
| Hierarchical organisation | Related classes are arranged in a logical tree |
| Extensibility | New subclasses can be added without touching existing code |
| Reduced redundancy | Avoids copy-pasting the same attributes/methods across similar classes |
A subclass can override an inherited method by supplying its own implementation with the same signature. At the design level you mark which methods each subclass redefines; the Programming course covers the language keyword and call mechanics in depth.
class Animal:
def speak(self):
return "..." # generic default
class Dog(Animal):
def speak(self): # OVERRIDES Animal.speak()
return "Woof!"
class Cat(Animal):
def speak(self): # OVERRIDES Animal.speak()
return "Meow!"
Polymorphism ("many forms") lets objects of different subclasses be treated through a common superclass type, so the same call produces different behaviour depending on the actual object.
| Type | Description |
|---|---|
| Runtime (dynamic) polymorphism | Method overriding — the correct method is selected at runtime from the object's actual type |
| Compile-time (static) polymorphism | Method overloading — several methods share a name but differ in parameter lists |
The design payoff is open-ended extensibility: code written against the superclass works unchanged for subclasses that did not exist when it was written.
animals = [Dog("Rex"), Cat("Whiskers"), Dog("Bella")]
for animal in animals:
print(animal.speak()) # dispatches to Dog.speak() or Cat.speak()
# by the object's actual type, not the list's
Output: Woof!, Meow!, Woof!. The loop never names Dog or Cat; it relies only on the promise that every Animal can speak(). Add a Hamster subclass tomorrow and this loop handles it with no edit — that is the design value polymorphism buys.
| Benefit | Explanation |
|---|---|
| Flexibility | Code using the superclass type works with any present or future subclass |
| Extensibility | New subclasses slot in without changing existing code |
| Cleaner code | Replaces sprawling if type == ... chains with a single call |
Abstraction means hiding complex implementation behind a simple, essential interface. In design, the key construct is the abstract class (or interface): a type that declares methods all subclasses must provide, without itself supplying the implementation.
| Concept | Detail |
|---|---|
| Abstract class | Cannot be instantiated directly; declares abstract methods that subclasses must implement |
| Interface | A pure specification of methods a class must provide, with no implementation |
| Purpose | Forces every subclass to honour a common contract while leaving the how to each |
from abc import ABC, abstractmethod
class Shape(ABC): # ABC = abstract base class
@abstractmethod
def area(self): ... # declared, not implemented
@abstractmethod
def perimeter(self): ...
class Circle(Shape):
def __init__(self, radius):
self.__radius = radius
def area(self):
return 3.14159 * self.__radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.__width = width
self.__height = height
def area(self):
return self.__width * self.__height
You cannot create a bare Shape, but every Circle and Rectangle is guaranteed to have area() and perimeter() — so a routine that totals the area of a list of Shape objects is safe. Abstraction and polymorphism work together: the abstract type defines the contract, polymorphism dispatches to the right implementation.
A practical first step in designing a hierarchy is to ask, for each candidate class, what is it responsible for and which other classes must it collaborate with. Two heuristics flow from this:
| Relationship | Description | Example | Strength |
|---|---|---|---|
| Association | A general "uses/knows" link between two classes | A Teacher teaches a Student | Weak — both exist independently |
| Aggregation | A "HAS-A" where the part can outlive the whole | A University HAS-A Department; close the university and departments could move elsewhere | Medium |
| Composition | A strong "HAS-A" where the part cannot exist without the whole | A House HAS-A Room; destroy the house and its rooms cease to exist | Strong |
In a class diagram these are drawn differently: a plain line (association), an open diamond at the "whole" end (aggregation), and a filled diamond (composition). Inheritance uses a hollow triangle pointing at the superclass.
Exam Tip: When designing a hierarchy under exam conditions, draw the superclass at the top and subclasses below, list each class's key attributes (with visibility) and methods, and label every relationship line with the correct symbol. Marks are awarded for the relationships, not just the boxes.
A top-band design answer recognises that OO is not always the right paradigm.
OO design fits well when:
OO design is a poor fit when:
The honest summary is that OOP excels at modelling a domain of interacting entities and at managing change in large systems, and is overkill for small, self-contained, computation-heavy tasks. Naming that trade-off is exactly the evaluative move examiners reward.
Scenario: model the people in a school so that students hold grades, teachers hold a subject and salary, and a head teacher is a special kind of teacher who also manages the school.
Step 1 — find the classes. The nouns suggest Person, Student, Teacher, HeadTeacher and School.
Step 2 — find the common state. Every person has a name, date of birth and email, so those belong on a shared Person superclass, not repeated on each subclass.
Step 3 — apply the IS-A test. A Student IS-A Person ✔; a Teacher IS-A Person ✔; a HeadTeacher IS-A Teacher ✔ (it adds management duties to everything a teacher does). A School, however, has students and teachers — that is aggregation, not inheritance.
classDiagram
class Person {
-name : String
-dateOfBirth : Date
-email : String
+getName() String
+getAge() int
}
class Student {
-studentID : String
-yearGroup : int
-grades : List
+addGrade(g) void
+getAverageGrade() float
}
class Teacher {
-employeeID : String
-subject : String
-salary : float
+teach() void
+getSalary() float
}
class HeadTeacher {
-schoolName : String
+manageSchool() void
}
Person <|-- Student
Person <|-- Teacher
Teacher <|-- HeadTeacher
School o-- Student
School o-- Teacher
Step 4 — check the pillars are working for you. Encapsulation: salary and grades are private, changed only through validated methods. Inheritance: name/DOB/email defined once. Polymorphism: a routine that prints getName() for a list of Person handles students, teachers and head teachers uniformly. Abstraction: outside code treats everyone simply as a Person when it only needs identity.
This four-step routine — find classes → factor common state up → apply the IS-A test → check the pillars — is a reliable method for any "design a class hierarchy" question.
The hardest part of a design question is often the first step: turning a paragraph of English into a set of classes. A reliable technique is noun-and-verb analysis.
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.