Exercises: OOP Design: Patterns and Principles

These exercises progress from identifying design concepts to implementing patterns and refactoring real code. All code should be Python 3.12+.

Difficulty Guide: - ⭐ Foundational (5-10 min each) - ⭐⭐ Intermediate (10-20 min each) - ⭐⭐⭐ Challenging (20-40 min each) - ⭐⭐⭐⭐ Advanced/Research (40+ min each)


Part A: Principles and Concepts ⭐

A.1. For each of the following class descriptions, determine whether it follows or violates the Single Responsibility Principle. If it violates SRP, identify the separate responsibilities. - a) UserAuthenticator — verifies passwords and manages login sessions - b) TemperatureConverter — converts between Celsius, Fahrenheit, and Kelvin - c) ShoppingCart — tracks items, calculates totals, processes payment, sends receipt email, and updates inventory - d) DateFormatter — formats date objects into various string representations

A.2. Define coupling and cohesion in your own words. For each, give one example of the "good" version and one of the "bad" version from code you've written in previous chapters.

A.3. Match each pattern to the problem it solves: | Pattern | Problem | |---------|---------| | Strategy | a) Multiple objects need to react when one object changes state | | Observer | b) You need to create objects without specifying the exact class | | Factory | c) You need to swap algorithms at runtime without changing the using code |

A.4. Explain the difference between a @dataclass and a regular class. When would you use each one? Give a specific example of a class that should be a dataclass and one that shouldn't.

A.5. List three code smells from Section 16.8. For each, write a one-sentence description of what it looks like and why it's a problem.

A.6. True or false, with explanation: - a) Every program should use design patterns. - b) A class that's 500 lines long is always a God Class. - c) Refactoring changes the behavior of your code. - d) Functions can follow the Single Responsibility Principle, not just classes. - e) The Observer pattern requires inheritance.


Part B: Design Analysis ⭐⭐

B.1. Consider this class:

class MusicPlayer:
    def __init__(self):
        self.playlist = []
        self.current_index = 0
        self.volume = 50
        self.equalizer = {"bass": 0, "mid": 0, "treble": 0}

    def add_song(self, song): ...
    def remove_song(self, song): ...
    def play(self): ...
    def pause(self): ...
    def next_track(self): ...
    def set_volume(self, level): ...
    def adjust_bass(self, level): ...
    def adjust_treble(self, level): ...
    def save_playlist(self, filename): ...
    def load_playlist(self, filename): ...
    def stream_to_bluetooth(self, device): ...
    def download_lyrics(self, song): ...
    def share_on_social_media(self, platform, song): ...

Identify at least four separate responsibilities. Sketch a design (class names and their methods — no implementation needed) that splits this into focused classes.

B.2. Rate the coupling between these two classes as "tight" or "loose" and explain why:

class Order:
    def __init__(self):
        self.items = []
        self._discount_code = None
        self._internal_tax_rate = 0.08

class OrderPrinter:
    def print_receipt(self, order):
        subtotal = sum(item.price for item in order.items)
        tax = subtotal * order._internal_tax_rate
        if order._discount_code == "SAVE10":
            subtotal *= 0.9
        print(f"Subtotal: ${subtotal:.2f}")
        print(f"Tax: ${tax:.2f}")

Rewrite both classes to achieve loose coupling.

B.3. The following code uses if/elif to select behavior. Refactor it to use the Strategy pattern:

class Sorter:
    def sort(self, data, algorithm="bubble"):
        if algorithm == "bubble":
            # Bubble sort implementation
            arr = list(data)
            for i in range(len(arr)):
                for j in range(len(arr) - i - 1):
                    if arr[j] > arr[j + 1]:
                        arr[j], arr[j + 1] = arr[j + 1], arr[j]
            return arr
        elif algorithm == "selection":
            # Selection sort implementation
            arr = list(data)
            for i in range(len(arr)):
                min_idx = i
                for j in range(i + 1, len(arr)):
                    if arr[j] < arr[min_idx]:
                        min_idx = j
                arr[i], arr[min_idx] = arr[min_idx], arr[i]
            return arr
        elif algorithm == "insertion":
            arr = list(data)
            for i in range(1, len(arr)):
                key = arr[i]
                j = i - 1
                while j >= 0 and arr[j] > key:
                    arr[j + 1] = arr[j]
                    j -= 1
                arr[j + 1] = key
            return arr

B.4. Convert each of the following into a @dataclass. Include appropriate type hints and defaults: - a) A Book with title (str), author (str), pages (int), isbn (str), and an optional rating (float, default 0.0) - b) A Point3D with x, y, z coordinates (all float). It should be immutable. - c) A Config with host (str, default "localhost"), port (int, default 8080), debug (bool, default False), and tags (list of str, default empty list)

B.5. Identify the code smells in this function and list specific refactoring steps to fix each one:

def process_order(name, email, phone, street, city, state, zip_code,
                  item_name, item_price, item_qty, item_weight,
                  shipping_method, payment_type, card_number):
    # Calculate total
    subtotal = item_price * item_qty
    if item_qty > 10:
        subtotal = subtotal * 0.9
    tax = subtotal * 0.08
    if state == "OR" or state == "MT" or state == "NH":
        tax = 0
    total = subtotal + tax

    # Calculate shipping
    if shipping_method == "standard":
        shipping = item_weight * 0.5
    elif shipping_method == "express":
        shipping = item_weight * 1.5
    elif shipping_method == "overnight":
        shipping = item_weight * 3.0

    total += shipping

    # Process payment
    if payment_type == "credit":
        print(f"Charging card {card_number} for ${total:.2f}")
    elif payment_type == "debit":
        print(f"Debiting card {card_number} for ${total:.2f}")

    # Send confirmation
    print(f"Order confirmed for {name}")
    print(f"Ship to: {street}, {city}, {state} {zip_code}")
    print(f"Total: ${total:.2f}")

Part C: Pattern Implementation ⭐⭐⭐

C.1. Implement a Strategy pattern for a text formatter. Create strategies for: - UpperCaseStrategy — converts text to uppercase - TitleCaseStrategy — converts text to title case - SlugStrategy — converts to lowercase with hyphens replacing spaces - CamelCaseStrategy — converts "hello world" to "helloWorld"

Create a TextProcessor context class that can apply any strategy. Test with the input "hello beautiful world".

C.2. Implement an Observer pattern for a weather station. The WeatherStation tracks temperature and humidity. When either changes, notify: - PhoneDisplay — prints a short summary - WebDashboard — prints an HTML snippet - AlertSystem — prints a warning if temperature exceeds 100 or humidity exceeds 90

Test by setting temperature to 75, then 105, and humidity to 85, then 95.

C.3. Implement a Factory pattern for creating shapes. Support Circle, Rectangle, and Triangle. Each shape should have: - An area() method - A perimeter() method - A describe() method that returns a string

The factory should accept a shape name and relevant dimensions (e.g., ShapeFactory.create("circle", radius=5)).

C.4. Combine the Observer and Strategy patterns: create a StockPortfolio that uses the Observer pattern to notify watchers when stock prices change, and the Strategy pattern to allow different alert strategies (e.g., PercentChangeAlert triggers on >5% change, ThresholdAlert triggers when price crosses a threshold).

C.5. Create a @dataclass called StudentRecord with fields for name, student_id, courses (list of strings), gpa (float), and enrollment_date (str). Then: - a) Make it sortable by GPA (descending — highest GPA first) - b) Create a frozen version called TranscriptEntry with course_name, grade, and credits - c) Write a function that takes a list of StudentRecord objects and returns them grouped by GPA range (3.5+, 3.0-3.49, 2.5-2.99, below 2.5)


Part D: Design Challenges ⭐⭐⭐⭐

D.1. Full Refactoring Exercise: Take this monolithic class and refactor it into a well-designed system using principles and patterns from this chapter. Write tests for your refactored version.

class LibrarySystem:
    def __init__(self):
        self.books = []
        self.members = []
        self.loans = []
        self.fines = {}
        self.printer_type = "laser"

    def add_book(self, title, author, isbn, copies): ...
    def remove_book(self, isbn): ...
    def search_by_title(self, title): ...
    def search_by_author(self, author): ...
    def register_member(self, name, email, member_type): ...
    def checkout_book(self, member_id, isbn): ...
    def return_book(self, member_id, isbn): ...
    def calculate_fine(self, member_id): ...
    def process_payment(self, member_id, amount): ...
    def send_overdue_notice(self, member_id): ...
    def print_report(self, report_type): ...
    def generate_statistics(self): ...
    def backup_to_database(self): ...
    def export_to_csv(self): ...

Your refactored design should: - Follow SRP (at least 4 separate classes) - Use at least one design pattern (Strategy, Observer, or Factory) - Use dataclasses where appropriate - Have loose coupling between components

D.2. Pattern Composition: Build a simple plugin system for a text editor. The editor should: - Use the Factory pattern to create plugins by name - Use the Observer pattern so plugins are notified when text changes - Use the Strategy pattern so the save behavior can be swapped (save to file, save to cloud, auto-save)

Implement at least three plugins: WordCounter (counts words on text change), SpellChecker (flags words not in a small dictionary), and AutoCapitalizer (capitalizes the first letter of each sentence).

D.3. Refactoring Crypts of Pythonia: Take the combat system from the Strategy pattern example in Section 16.4 and extend it: - Add an inventory system that affects combat (weapons modify attacker_power, armor modifies defender_armor) - Use the Observer pattern to log combat events - Use dataclasses for items - Add a CombatStrategyFactory that creates strategies based on character class name - Write at least 5 unit tests using pytest

D.4. Research the Decorator pattern (not Python's @decorator syntax — the OOP design pattern). Implement a coffee ordering system where a base Coffee object can be "decorated" with add-ons (milk, sugar, whipped cream, etc.) that modify the description and price. Compare this approach to using inheritance (how many subclasses would you need for every combination?).

D.5. The Design Debate: Write a 300-500 word essay arguing against using OOP for a specific type of project. Choose one of: - a) A data analysis pipeline that reads CSV files, cleans data, computes statistics, and generates a report - b) A simple web scraper that collects prices from three websites - c) A command-line utility that renames files based on a pattern

Include concrete code examples showing the functional approach and explain why OOP would add unnecessary complexity for that specific use case.