B3.2.5 · Fundamentals of OOP for multiple classes (HL)
Design patterns in OOP
Three problems that come up in almost every large program, and the shapes programmers have agreed on for solving them.
Explainer
Calderbrook Interchange is building the software behind its departure boards. Three problems come up almost immediately, and none of them is really about buses. There must be exactly one departure log, however many parts of the system want to write to it. Alerts come in several kinds, and the code raising one should not have to know which class to build. And when a service is delayed, several very different displays all need telling at once, without the service knowing what a display even is.
A design pattern is a named, reusable solution to a problem that keeps recurring across different programs. It is not code you copy, it is a shape you apply: a description of which classes exist, what each is responsible for, and how they talk to each other.
Two benefits are worth naming in an answer. Patterns are proven, so you inherit a solution whose weaknesses are already understood rather than inventing one. And they are named, which gives developers shared vocabulary: saying "make it a singleton" communicates an entire design in three words.
The three below are the ones the syllabus asks for. Each is introduced by the recurring problem it exists to answer, because that problem is what the second elaboration bullet is really asking about.
Pattern 1
Singleton
Something in the system must exist exactly once. If two copies are created by accident, they drift apart and neither is right. At Calderbrook the departure log is like this: two logs would mean two different answers to "what happened this morning?"
Under the singleton pattern, a class is written so that it can never be instantiated twice: the first request creates the object, every later request receives that same one, and the class exposes a single agreed route to it. The class itself takes responsibility for preventing a second instance, rather than trusting every caller to remember.
class DepartureLog: _instance = None # the one instance lives on the CLASS def __init__(self): self.entries = [] @staticmethod def get(): # the single agreed way in if DepartureLog._instance == None: DepartureLog._instance = DepartureLog() return DepartureLog._instance def record(self, text): self.entries.append(text) # every caller gets the SAME object, so every entry lands in one place DepartureLog.get().record("Service 12 delayed") DepartureLog.get().record("Service 4 cancelled")
The give-away is a class variable holding an instance of its own class, plus a method that hands it out. Note that get() is a static method: it has to work before any instance exists, so it cannot need one.
Pattern 2
Factory
Several related classes exist, and the code that needs one should not have to choose between them. At Calderbrook a delay, a cancellation and a platform change are three different alert classes, but the code reporting an incident only knows a string from an operator's form.
The factory pattern puts object creation behind a single method that decides which class to instantiate and returns it. The caller asks for what it wants by description and receives a ready-made object, without naming any concrete class itself.
class AlertFactory: @staticmethod def create(kind, detail): # ONE entry point for three classes if kind == "delay": return DelayAlert(detail) elif kind == "cancelled": return CancellationAlert() elif kind == "platform": return PlatformChangeAlert(detail) return None # the caller never writes DelayAlert(...) or CancellationAlert() alert = AlertFactory.create("delay", 7) print(alert.text())
Every alert class offers a text() method, so whatever comes back can be used the same way. That shared interface is what makes the factory safe, and it is abstraction and polymorphism doing the work underneath.
A constructor builds one known class, and the caller has to name it. A factory decides which class to build. Adding a fourth alert type means editing the factory only, while every place that reports an incident keeps working untouched.
Pattern 3
Observer
When one object changes, an unknown number of others need to react, and the changing object should not have to know who they are. At Calderbrook a delayed service must reach the platform screen, the mobile app and the announcement system, and next year something else nobody has built yet.
The observer pattern lets one object (the subject) keep a list of dependent objects (the observers) and notify every one of them automatically when its state changes. Observers subscribe and unsubscribe at run time, and the subject knows nothing about them beyond the fact that they can be notified.
class BusService: # the SUBJECT def __init__(self, code): self.code = code self.observers = [] # who wants telling def subscribe(self, observer): self.observers.append(observer) def unsubscribe(self, observer): self.observers.remove(observer) def announce(self, message): # tell everyone, whoever they are for o in self.observers: o.update(self.code, message) s12 = BusService("Service 12") s12.subscribe(PlatformScreen("Platform 3")) # an OBSERVER s12.subscribe(MobileApp("Calderbrook app")) # another one s12.announce("delayed by 7 minutes")
Notice what BusService does not contain: no mention of screens, apps or announcements. It only requires that anything on its list has an update() method. That is what lets a new display type be added years later without touching this class at all.
Explainer
Recognising each one
An exam question usually gives you a situation and asks which pattern fits. Match on the problem, not on the vocabulary.
| Pattern | The recurring challenge it solves | What to look for in code |
|---|---|---|
| Singleton | Exactly one of something must exist, and everything must share it | A class variable holding an instance of its own class, reached through a static method such as get() |
| Factory | Choosing between several related classes, without the caller naming any of them | One method that returns different classes depending on its argument |
| Observer | One change must reach many objects, and their number is not known in advance | A list of subscribers, a subscribe method, and a loop that calls the same method on each |
Each one removes a dependency. The caller stops depending on when an object was created, or on which class it is, or on who needs telling. That is why patterns make a system easier to extend: the parts know less about each other than they otherwise would.
Try it yourself
All three, in one departure board
The Calderbrook system, live and editable. Worth trying: add a third observer class and subscribe it without touching BusService, and check the singleton really is one object by recording through one handle and reading through another.
Running in offline fallback mode. The full Python engine couldn’t load (blocked school network, or no internet), so this console switched automatically to a small hand-written engine. It runs this page’s scenario, including static methods, list subscription and object factories, but it isn’t full Python.
Check your understanding
A weather app must alert an unknown number of widgets whenever a new reading arrives, and new widget types are added regularly without changing the reading code. Which pattern fits, and why?
Common mistake
B3.2.5RecurringCandidates describe the mechanism of a pattern accurately and never say which problem it solves. "A factory has a method that creates objects" is true of every constructor ever written, so on its own it earns little. The mark is in the purpose: the caller does not have to know which class it is getting.
The second error is treating singleton as a synonym for "a class with static methods". A class of static methods holds no state and is never instantiated; a singleton is a real object with real attributes, of which exactly one exists.
Answer frame that scores: name the recurring problem → name the pattern → say what the pattern removes the need for the rest of the program to know.
What a weak answer looks like: “The observer pattern is when objects observe another object and get updated.” This restates the name without naming a problem, and would fit almost any event-driven code.
What it should say: “When a service is delayed, several displays must react, but the number and type of displays change over time. The observer pattern lets each display subscribe to the service, so the service notifies its list without knowing what any of them are, and a new display type can be added without editing BusService.” Problem, pattern, and what it removes.
Linking question
How this connects to the rest of the syllabus
The IB syllabus is deliberately woven together, not a stack of isolated topics. Here's how design patterns connect specifically to A4 (machine learning).
How can design patterns in OOP facilitate the architecture of scalable and maintainable machine learning models? (All three patterns on this page appear in real ML systems: a single shared model registry so every request scores against the same loaded weights, a factory that returns whichever model version a request asks for without the caller naming it, and observers that react when training finishes, one logging metrics, another retiring the previous model.)
Revision questions
Past-paper style. Write an answer, then reveal the marking points and mark yourself.
[3]Explain why DepartureLog is implemented as a singleton, referring to what would go wrong without it.
Marking points
评分要点
- DepartureLog must be the single shared record of departures for the whole system.
- The singleton pattern ensures only one instance is ever created, with one global access point.
- Without it, different parts of the system could each make their own DepartureLog, so recorded departures would be split across separate objects and no single log would be complete or consistent.
- DepartureLog 必须是整个系统唯一的、共享的发车记录。
- 单例模式确保只创建一个实例,并有一个全局访问点。
- 没有它,系统的不同部分可能各自创建自己的 DepartureLog,于是记录的发车会分散在多个独立对象中,没有一份日志是完整或一致的。
[4]Calderbrook adds a DiversionAlert. Construct the change needed, and state which parts of the existing system must be edited and which must not.
Marking points
评分要点
- Create a DiversionAlert class with an update() method, matching the interface every observer already has.
- Register a DiversionAlert instance with the BusService (subject) by adding it to the observer list.
- Must be edited: the code that constructs and registers observers, to add the new one.
- Must not be edited: BusService itself, the other alert classes, and the notify logic, because the subject calls update() on every observer without knowing their types.
- 创建一个带 update() 方法的 DiversionAlert 类,匹配每个观察者已有的接口。
- 通过把一个 DiversionAlert 实例加入观察者列表,把它注册到 BusService(主体)。
- 必须修改:构造并注册观察者的代码,以添加新的那个。
- 不得修改:BusService 本身、其他警报类以及通知逻辑,因为主体在不知道观察者类型的情况下对每个观察者调用 update()。
[5]A developer proposes that BusService should call each display directly instead of keeping a list of observers. Evaluate this proposal.
Marking points
评分要点
- Calling each display directly means BusService must name every display type in its own code.
- Adding a new display would then require editing BusService, which breaks the open-closed principle.
- It also couples BusService to concrete classes it should not need to know about, making it harder to test and reuse.
- The observer list lets BusService notify any number of observers of any type through a common update() method, with no change when observers are added or removed.
- Evaluation: reject the proposal; the observer list is the better design.
- 直接调用每个显示器意味着 BusService 必须在自己的代码中指名每一种显示类型。
- 那么添加一个新显示器就需要修改 BusService,这违反开闭原则。
- 它还把 BusService 与它本不需要知道的具体类耦合,使其更难测试和复用。
- 观察者列表让 BusService 通过一个共同的 update() 方法通知任意数量、任意类型的观察者,在添加或移除观察者时无需改动。
- 评价:拒绝该提案;观察者列表是更好的设计。
Flashcards
Active recall for this statement’s key terms.
Keep this statement’s practice with you
Free account: unlock every revision question and flashcard for B3.2.5, and every other IB CS 2027 statement.