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:* Good organization and SRP are separate concepts. *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 is separate from the pattern's structure. *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. `__hash__` is only generated if `frozen=True` or `eq=False`. *Why not A:* It generates more than just `__init__`. *Why not C:* `__hash__` is set to `None` by default (because mutable objects with `__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. **Tight coupling** in a restaurant: the chef can only cook if they personally walk to the dining room, ask customers for their orders, and bring back ingredients from the market themselves. If the waiter is sick, the chef can't work. **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 — orders on tickets, not direct dependencies) 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):
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.
@dataclass
class Team:
name: str
members: list[str] = []
founded: int = 2024
Answer
The **mutable default** `[]` for `members` is the problem. All instances would share the same list object. This will actually raise a `ValueError` at class definition time in Python — dataclasses explicitly prevent mutable defaults. **Fixed version:**from 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 6 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.