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.2

2. 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.2

3. 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.3

4. 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.3

5. 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.4

6. 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.5

7. 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.7

8. 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.8

9. 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.9

10. 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.6

Section 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.3

12. 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.4

13. 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 default for field members is not allowed: use default_factory`. **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.5

Section 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.4

18. 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.5

20. 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.

Answer **Strategy pattern is better here** because: 1. **Flexibility:** An enemy might switch attack types during combat (e.g., a mage who draws a sword when out of mana). With inheritance, a `MagicEnemy` is always magical. With Strategy, you call `enemy.set_strategy(MeleeStrategy())` and the behavior changes instantly. 2. **Combinability:** What about an enemy that can do both ranged and melee? With inheritance, you'd need `MeleeRangedEnemy`, `MagicMeleeEnemy`, etc. — a combinatorial explosion. With Strategy, you compose behaviors freely. 3. **Extensibility (OCP):** Adding a new attack type means one new Strategy class. No existing code changes. **When inheritance IS better:** If the difference between enemy types goes beyond just attack behavior — if `FlyingEnemy` has different movement physics, different health regeneration, different AI, and different rendering — then the enemies are fundamentally different *kinds* of things, and inheritance captures that structural difference well. Use inheritance for "is-a" relationships that affect the whole object, and Strategy for swappable behaviors. *Reference:* Sections 16.4, 16.2