Quiz: OOP Design: Patterns and Principles
Test your understanding before moving on. Target: 70% or higher to proceed confidently.
Section 1: Multiple Choice (1 point each)
1. What does the Single Responsibility Principle state?
- A) A class should have only one method
- B) A class should have only one reason to change
- C) A function should take only one parameter
- D) A module should contain only one class
Answer
**B)** A class should have only one reason to change. *Why B:* SRP is about responsibilities (reasons to change), not about limiting methods, parameters, or classes per module. *Why not A:* A class can have many methods as long as they all relate to the same responsibility. *Why not C:* SRP applies to classes (and functions), but it's about responsibility scope, not parameter count. *Why not D:* Modules often contain multiple related classes. *Reference:* Section 16.22. Which of the following describes the Open/Closed Principle?
- A) Classes should be open for modification and closed for extension
- B) Open-source code should have closed documentation
- C) Classes should be open for extension but closed for modification
- D) Methods should be open (public) or closed (private), never both
Answer
**C)** Classes should be open for extension but closed for modification. *Why C:* OCP means you add new behavior by creating new classes (extension), without changing existing working code (modification). *Why not A:* This is the exact opposite of OCP. *Why not B/D:* These are not related to OCP at all. *Reference:* Section 16.23. What does "tight coupling" mean?
- A) Classes are well-organized and follow SRP
- B) Classes depend heavily on each other's internal details
- C) Classes have high cohesion
- D) Classes use abstract base classes
Answer
**B)** Classes depend heavily on each other's internal details. *Why B:* Tight coupling means changes to one class force changes to another because they depend on each other's internals. *Why not A:* SRP is about how focused each class is; coupling is about how classes depend on one another. Well-organized classes can still be tightly coupled. *Why not C:* High cohesion is about how focused a single class is, not about dependencies between classes. *Why not D:* Using ABCs can actually reduce coupling by creating interfaces. *Reference:* Section 16.34. A class has methods for sending emails, calculating taxes, and resizing images. This class has:
- A) High cohesion
- B) Low cohesion
- C) Loose coupling
- D) Strong encapsulation
Answer
**B)** Low cohesion. *Why B:* These three tasks are completely unrelated — they don't share any data or purpose. Low cohesion means the class is a grab-bag of unrelated functionality. *Why not A:* High cohesion means all methods relate to one purpose. *Why not C:* Coupling describes relationships between classes, not within one class. *Why not D:* Encapsulation is about hiding internals, not about method relatedness. *Reference:* Section 16.35. In the Strategy pattern, what is the "context"?
- A) The abstract base class that defines the strategy interface
- B) The class that uses a strategy object to delegate behavior
- C) The concrete implementation of a specific algorithm
- D) The client code that chooses which strategy to use
Answer
**B)** The class that uses a strategy object to delegate behavior. *Why B:* The context holds a reference to a strategy and delegates work to it. In the text adventure example, `Character` is the context. *Why not A:* That's the strategy interface (e.g., `CombatStrategy`). *Why not C:* That's a concrete strategy (e.g., `AggressiveStrategy`). *Why not D:* The client code picks a strategy and hands it to the context (like the lines that call `hero.set_strategy(...)`), but it isn't the context itself. *Reference:* Section 16.46. When should you use the Observer pattern?
- A) When you need to create objects of different types based on input
- B) When you need to swap algorithms at runtime
- C) When multiple objects need to react to another object's state changes
- D) When you need to make immutable data objects
Answer
**C)** When multiple objects need to react to another object's state changes. *Why C:* Observer connects a subject (the thing changing) to observers (things that react), without tight coupling. *Why not A:* That's the Factory pattern. *Why not B:* That's the Strategy pattern. *Why not D:* That's the use case for `@dataclass(frozen=True)`. *Reference:* Section 16.57. What does the @dataclass decorator auto-generate?
- A) Only
__init__ - B)
__init__,__repr__, and__eq__ - C)
__init__,__repr__,__eq__, and__hash__ - D) All dunder methods
Answer
**B)** `__init__`, `__repr__`, and `__eq__`. *Why B:* By default, `@dataclass` generates these three methods. A `__hash__` is generated only when you ask for it — for example with `frozen=True` (alongside the default `eq=True`) or with `unsafe_hash=True`. *Why not A:* It generates more than just `__init__`. *Why not C:* With the default settings, `__hash__` is set to `None`, which makes instances unhashable (mutable objects that define `__eq__` shouldn't be hashable). *Why not D:* It generates a specific subset, not all dunder methods. *Reference:* Section 16.78. What is a "code smell"?
- A) A syntax error that prevents code from running
- B) A runtime exception caused by bad input
- C) A surface indicator of a deeper design problem, even though the code works
- D) Code that has poor performance
Answer
**C)** A surface indicator of a deeper design problem, even though the code works. *Why C:* Code smells are not bugs — the code runs correctly. They're warning signs that the design might cause problems as the code evolves. *Why not A:* Syntax errors prevent execution; smells are in working code. *Why not B:* Runtime exceptions are bugs, not smells. *Why not D:* Poor performance is a different concern (optimization), though it can sometimes correlate. *Reference:* Section 16.89. Which of the following is the best reason to choose plain functions over OOP?
- A) You want to use polymorphism
- B) You're writing a short data transformation script with no persistent state
- C) You need multiple instances that maintain their own state
- D) You're building a library that others will extend
Answer
**B)** You're writing a short data transformation script with no persistent state. *Why B:* Functions excel at stateless transformations — data goes in, results come out. No need for the overhead of classes. *Why not A:* Polymorphism is a key reason to use OOP. *Why not C:* Multiple stateful instances is a key reason to use OOP. *Why not D:* Extensible libraries benefit from OOP's inheritance and polymorphism. *Reference:* Section 16.910. In the Factory pattern, what is the main benefit of the register() method (as shown in Section 16.6)?
- A) It makes the factory faster
- B) It allows adding new types without modifying the factory's source code
- C) It validates that new types are correct
- D) It removes the need for abstract base classes
Answer
**B)** It allows adding new types without modifying the factory's source code. *Why B:* `register()` enables the Open/Closed Principle — new formatters can be added by calling `register()` from external code, without editing the factory class itself. *Why not A:* It doesn't affect performance. *Why not C:* Registration doesn't inherently validate (though it could). *Why not D:* ABCs are still useful for defining the interface. *Reference:* Section 16.6Section 2: Short Answer (2 points each)
11. Explain the difference between coupling and cohesion using a restaurant analogy. What would "tight coupling" and "low cohesion" look like in a restaurant?
Answer
**Coupling** is how much different parts depend on each other's details. **Tight coupling** in a restaurant: the chef can only read one particular waiter's private shorthand, scribbled on napkins. If that waiter is sick, or simply changes how they abbreviate dishes, the kitchen grinds to a halt. **Cohesion** is how focused one part is on its own job. **Low cohesion** in a restaurant: the chef also does accounting, repairs the plumbing, and handles advertising — tasks that have nothing to do with cooking. Good design: loose coupling (chef, waitstaff, and manager interact through clear roles — standard order tickets that any waiter can write and any cook can read) and high cohesion (the chef focuses on cooking, the manager focuses on operations). *Reference:* Section 16.312. Look at this code and identify which design pattern it implements. Name the three roles (using the pattern's terminology):
from abc import ABC, abstractmethod
class PricingRule(ABC):
@abstractmethod
def calculate(self, base_price: float) -> float: ...
class RegularPricing(PricingRule):
def calculate(self, base_price: float) -> float:
return base_price
class HolidaySale(PricingRule):
def calculate(self, base_price: float) -> float:
return base_price * 0.8
class Product:
def __init__(self, name, base_price, pricing: PricingRule):
self.name = name
self.base_price = base_price
self.pricing = pricing
def get_price(self):
return self.pricing.calculate(self.base_price)
Answer
This is the **Strategy pattern**. - **Strategy interface:** `PricingRule` (ABC defining the `calculate` method) - **Concrete strategies:** `RegularPricing` and `HolidaySale` (specific implementations) - **Context:** `Product` (holds a strategy and delegates pricing calculation to it) The pricing algorithm can be swapped at runtime by changing the `Product`'s `pricing` attribute. *Reference:* Section 16.413. What's wrong with this dataclass definition? Fix it.
from dataclasses import dataclass
@dataclass
class Team:
name: str
members: list[str] = []
founded: int = 2024
Answer
The **mutable default** `[]` for `members` is the problem. If Python allowed it, every `Team` would share one list object, so adding a member to one team would add them to all of them. Dataclasses catch this for you: the class definition itself fails with `ValueError: mutable defaultfrom dataclasses import dataclass, field
@dataclass
class Team:
name: str
members: list[str] = field(default_factory=list)
founded: int = 2024
`field(default_factory=list)` creates a new empty list for each instance.
*Reference:* Section 16.7
14. A student writes this code and says, "I'm using the Observer pattern!" Are they correct? Why or why not?
class Logger:
def log(self, message):
print(f"[LOG] {message}")
class App:
def __init__(self):
self.logger = Logger()
def do_something(self):
self.logger.log("Did something")
Answer
**No, this is not the Observer pattern.** This is simple composition — `App` directly creates and calls `Logger`. In the Observer pattern: 1. The subject maintains a **list** of observers (not a single hardcoded one) 2. Observers can be **added and removed** dynamically at runtime 3. The subject calls a **common interface method** on all observers when state changes 4. The subject **doesn't know the concrete types** of its observers This code has tight coupling between `App` and `Logger`. To be the Observer pattern, `App` would need an `add_observer()` method, a list of observers, and a notification loop. *Reference:* Section 16.5Section 3: Code Analysis (3 points each)
15. What will this code print? Trace through the execution:
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def notify(self, message: str) -> None:
pass
class EmailNotifier(Notifier):
def notify(self, message: str) -> None:
print(f"Email: {message}")
class SMSNotifier(Notifier):
def notify(self, message: str) -> None:
print(f"SMS: {message}")
class EventSystem:
def __init__(self):
self._subscribers: list[Notifier] = []
def subscribe(self, notifier: Notifier):
self._subscribers.append(notifier)
def emit(self, message: str):
for sub in self._subscribers:
sub.notify(message)
events = EventSystem()
events.subscribe(EmailNotifier())
events.subscribe(SMSNotifier())
events.subscribe(EmailNotifier())
events.emit("Server is down!")
Answer
Email: Server is down!
SMS: Server is down!
Email: Server is down!
Three observers are subscribed: two `EmailNotifier` instances and one `SMSNotifier`. When `emit()` is called, it iterates through all three subscribers and calls `.notify()` on each one, in the order they were added.
*Reference:* Section 16.5
16. This code uses the Factory pattern. What happens when create("xml") is called? What happens when create("json") is called?
class SerializerFactory:
_serializers = {
"json": lambda data: str(data),
"csv": lambda data: ",".join(str(v) for v in data),
}
@classmethod
def create(cls, format_name):
serializer = cls._serializers.get(format_name)
if serializer is None:
raise ValueError(f"Unknown format: {format_name}")
return serializer
fn = SerializerFactory.create("json")
print(fn({"a": 1, "b": 2}))
Answer
When `create("json")` is called, it looks up `"json"` in the `_serializers` dictionary, finds the lambda `lambda data: str(data)`, and returns it. Then `fn({"a": 1, "b": 2})` calls that lambda, which prints:{'a': 1, 'b': 2}
When `create("xml")` is called, `_serializers.get("xml")` returns `None`, so the code raises:
ValueError: Unknown format: xml
This is a simplified Factory pattern using functions (lambdas) instead of classes. The lookup dictionary centralizes the creation logic.
*Reference:* Section 16.6
17. Examine this refactoring. What specific improvements were made? Name the principles or patterns applied.
Before:
class Report:
def generate(self, data, output_type):
result = ""
for item in data:
result += f"{item['name']}: {item['value']}\n"
if output_type == "console":
print(result)
elif output_type == "file":
with open("report.txt", "w") as f:
f.write(result)
elif output_type == "html":
html = f"<pre>{result}</pre>"
with open("report.html", "w") as f:
f.write(html)
After:
class ReportFormatter:
def format(self, data):
return "".join(f"{item['name']}: {item['value']}\n" for item in data)
class ConsoleOutput:
def write(self, content):
print(content)
class FileOutput:
def __init__(self, filename):
self.filename = filename
def write(self, content):
with open(self.filename, "w") as f:
f.write(content)
class Report:
def __init__(self, formatter, output):
self.formatter = formatter
self.output = output
def generate(self, data):
content = self.formatter.format(data)
self.output.write(content)
Answer
Improvements made: 1. **Single Responsibility Principle:** The original `Report` class handled formatting AND output. Now `ReportFormatter` handles formatting, `ConsoleOutput`/`FileOutput` handle output, and `Report` coordinates them. 2. **Open/Closed Principle:** To add a new output type (e.g., email), you create a new class with a `write()` method — no modification to `Report` needed. 3. **Strategy pattern:** Both `formatter` and `output` are strategy objects — they can be swapped at runtime. `Report` delegates to whatever formatter and output it's given. 4. **Loose coupling:** `Report` doesn't know whether it's writing to console or file — it just calls `.write()` on whatever output object it has. 5. **Eliminated conditional logic:** The if/elif chain for output types is replaced by polymorphism. *Reference:* Sections 16.2, 16.3, 16.418. Given these two dataclasses, predict the output:
from dataclasses import dataclass, field
@dataclass(order=True)
class Priority:
level: int
label: str = field(compare=False)
tasks = [
Priority(3, "Low"),
Priority(1, "Critical"),
Priority(2, "Medium"),
Priority(1, "Also Critical"),
]
print(sorted(tasks))
print(Priority(1, "Critical") == Priority(1, "Also Critical"))
Answer
[Priority(level=1, label='Critical'), Priority(level=1, label='Also Critical'), Priority(level=2, label='Medium'), Priority(level=3, label='Low')]
True
Because `label` has `compare=False`, it's excluded from both ordering AND equality comparisons. Only `level` is used for comparisons. So:
- Sorting is purely by `level` (ascending: 1, 1, 2, 3)
- The two `Priority(1, ...)` objects are considered equal even though their labels differ
- The relative order of the two level-1 items is preserved from the original list (stable sort)
*Reference:* Section 16.7
Section 4: Design Judgment (3 points each)
19. A student proposes this design for a school management system. Evaluate it — what's good and what should change?
SchoolSystem (one class):
- add_student()
- remove_student()
- add_teacher()
- remove_teacher()
- assign_grade()
- calculate_gpa()
- generate_report_card()
- schedule_class()
- book_room()
- process_tuition_payment()
- send_newsletter()
- order_supplies()
Answer
**Assessment:** This is a classic **God Class** that violates SRP severely. It has at least seven separate responsibilities: 1. **Student management:** add/remove students 2. **Teacher management:** add/remove teachers 3. **Academic records:** assign grades, calculate GPA, report cards 4. **Scheduling:** schedule classes, book rooms 5. **Financial:** process tuition payments 6. **Communication:** send newsletters 7. **Operations:** order supplies **Recommended redesign:** - `StudentRegistry` — student CRUD operations - `TeacherRegistry` — teacher CRUD operations - `GradeBook` — grades, GPA, report cards - `Scheduler` — class scheduling, room booking - `BillingSystem` — tuition and payments - `CommunicationService` — newsletters, notifications - `SupplyManager` — supply ordering Each class has one responsibility and can change independently. You might use Observer pattern so that when a student is added to `StudentRegistry`, `BillingSystem` and `CommunicationService` are notified automatically. *Reference:* Sections 16.1, 16.2, 16.520. You're building a game where enemies can attack in different ways: melee, ranged, and magic. As the game grows, you'll add new attack types. A teammate suggests using inheritance:
class MeleeEnemy(Enemy): ...
class RangedEnemy(Enemy): ...
class MagicEnemy(Enemy): ...
Another suggests using the Strategy pattern. Which approach is better for this specific scenario? Explain your reasoning, and describe a scenario where the other approach would be better.