Intermediate Projects

Learning Objectives

Build an e-commerce cart with pricing strategies and order states.

Build a notification framework with interfaces, strategy, factory and DI.

Build a parking lot with pricing and state.

Build a streaming file-processing framework with strategies and safe resource handling.

Prerequisites: Stages 2–3.

Intuition and Definition

These projects combine several patterns where they genuinely help — and deliberately avoid inheritance where it would be pointless (a Car/Truck hierarchy for a parking lot that only cares about size is a classic over-design).

Why It Matters

Intermediate projects are where "pattern soup" usually starts. Practise justifying each abstraction.

Syntax and Language Notes

Project 5 is written in TypeScript and Project 7 in Python to practise the same ideas in other languages.

Complete Example

Project 4 — E-commerce Cart Requirements: products, line items with quantity, discount rules (percentage, buy-X-get-Y, threshold), order states New → Paid → Shipped (or Cancelled), totals in cents.

Project 5 — Notification Framework (TypeScript) Requirements: send via email, SMS or push; choose channels per user preference; retry transient failures; record delivery results; new channels must not change the dispatcher.

Project 6 — Parking Lot (pricing and state, no pointless inheritance)

Module 25 built the core. The intermediate version adds a pricing strategy per lot type, spot state (free/occupied/out of service) and tests. Vehicles are a value type with a size — not a class hierarchy — because nothing behaves differently per vehicle class.

Project 7 — File Processing Framework (Python) Requirements: stream large CAN trace files line by line; pluggable parsers per format; pluggable sinks; bad lines are reported, not fatal; files are always closed.

#include <algorithm>
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

struct Product { std::string sku; long long priceCents; };
struct Line { Product product; int qty; };

struct DiscountRule {                                         // Strategy
    virtual ~DiscountRule() = default;
    virtual long long discount(const std::vector<Line>& lines, long long subtotal) const = 0;
    virtual std::string name() const = 0;
};
struct PercentOff : DiscountRule {
    int pct; explicit PercentOff(int p) : pct(p) {}
    long long discount(const std::vector<Line>&, long long sub) const override { return sub * pct / 100; }
    std::string name() const override { return std::to_string(pct) + "% off"; }
};
struct BuyXGetY : DiscountRule {
    std::string sku; int x, y;
    BuyXGetY(std::string s, int xx, int yy) : sku(std::move(s)), x(xx), y(yy) {}
    long long discount(const std::vector<Line>& lines, long long) const override {
        for (const auto& l : lines) if (l.product.sku == sku) return (l.qty / (x + y)) * y * l.product.priceCents;
        return 0;
    }
    std::string name() const override { return "buy " + std::to_strin
…
subtotal 19800, total with best discount 15900
order of 15900 cents shipped
error: cannot cancel a shipped order
error: cannot ship now
interface Channel { readonly name: string; send(to: string, text: string): Promise<void>; }
class EmailChannel implements Channel { readonly name = "email"; async send(to: string, t: string) { console.log(`  [email] ${to}: ${t}`); } }
class SmsChannel implements Channel {
  readonly name = "sms"; private failuresLeft = 1;
  async send(to: string, t: string) { if (this.failuresLeft-- > 0) throw new Error("gateway timeout"); console.log(`  [sms] ${to}: ${t.slice(0, 30)}`); }
}
class PushChannel implements Channel { readonly name = "push"; async send(to: string, t: string) { console.log(`  [push] ${to}: ${t}`); } }
class ChannelRegistry {
  private factories = new Map<string, () => Channel>();
  register(name: string, f: () => Channel) { this.factories.set(name, f); return this; }
  create(name: string): Channel { const f = this.factories.get(name); if (!f) throw new Error(`no channel '${name}'`); return f(); }
}
interface User { id: string; contacts: Record<string, string>; prefers: string[]; }
class Dispatcher {
  private channels = new Map<string, Channel>();
  private registry: ChannelRegistry; private maxAttempts: number;
  constructor(registry: ChannelRegistry, maxAttempts = 3) 
…
[sms] +15550100: Your vehicle service is booked
  [email] [email protected]: Your vehicle service is booked for 9:00 tomorrow
[
  'sms: delivered (attempt 2)',
  'push: no contact',
  'email: delivered (attempt 1)'
]
#include <cassert>
#include <functional>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

enum class Size { S = 1, M, L };
enum class SpotState { Free, Occupied, OutOfService };
struct Vehicle { std::string plate; Size size; };                 // value, not a hierarchy
struct Spot {
    int id; Size size; SpotState state = SpotState::Free; std::string plate; int since = 0;
    Spot(int i, Size s) : id(i), size(s) {}
};

using Pricing = std::function<int(int minutes)>;                  // strategy as a function
Pricing hourly(int firstHour, int perHour) {
    return [=](int m) { int h = (m + 59) / 60; return h <= 1 ? firstHour : firstHour + (h - 1) * perHour; };
}
Pricing airportDaily(int perDay) { return [=](int m) { return ((m + 1439) / 1440) * perDay; }; }

class Lot {
    std::vector<Spot> spots_;
    Pricing price_;
public:
    Lot(std::vector<Spot> s, Pricing p) : spots_(std::move(s)), price_(std::move(p)) {}
    std::optional<int> enter(const Vehicle& v, int minute) {
        for (Spot& s : spots_)
            if (s.state == SpotState::Free && static_cast<int>(v.size) <= static_cast<int>(s.size)) {
                s.state = SpotState::Occupied; s.p
…
sedan in spot 3, van: no space
city fee for 130 min: 700
airport fee for 2 days 1 min: 7500
unknown plate: not found
import io
from abc import ABC, abstractmethod
from contextlib import closing

class Parser(ABC):                                    # strategy per input format
    @abstractmethod
    def parse(self, line: str): ...

class AscParser(Parser):                              # e.g. "0.0100 1 0C0 Rx d 8 FF 7D ..."
    def parse(self, line):
        parts = line.split()
        if len(parts) < 6 or parts[4] != "d": raise ValueError("not a data frame")
        return float(parts[0]), int(parts[2], 16), bytes(int(b, 16) for b in parts[6:6 + int(parts[5])])

class CsvParser(Parser):                              # "t,id,data_hex"
    def parse(self, line):
        t, can_id, data = line.split(",")
        return float(t), int(can_id, 16), bytes.fromhex(data)

PARSERS = {"asc": AscParser, "csv": CsvParser}        # factory registry

class Pipeline:
    def __init__(self, parser: Parser, sinks):
        self.parser, self.sinks, self.errors = parser, sinks, []
    def run(self, stream):
        with closing(stream):                         # resource always released
            for n, line in enumerate(stream, start=1):
                line = line.strip()
                if not line or line.star
…
{'0xc0': 3, '0x1a0': 1} speeds: [1000.0, 1200.0, 1440.0]
errors: ['line 5: not a data frame'] | stream closed: True

Step-by-Step Walkthrough

Cart: discount rules are strategies; the cart applies the *best single* rule — a documented business decision. The subtotal is 4500 + 3×3900 + 3×1200 = 19800; 10% gives 1980, buy-2-get-1 oil gives 3900, the threshold gives 1000 → 3900 is applied, total 15900. Order status transitions are guarded (a simple enum suffices).

Notifications: channels come from a factory registry and are injected into the dispatcher; the flaky SMS provider succeeds on the second attempt; adding a channel is one class plus one registration.

Parking: pricing is a function strategy per lot; spot state includes out-of-service; vehicles are values — no Car/Truck subclasses.

Files: parsers are strategies chosen by format; sinks are plain functions; the stream is always closed; malformed lines are collected, not fatal.

Common Mistakes

Stacking all discounts by accident (or not deciding the stacking rule at all).

A class per vehicle type with no behaviour differences.

Loading entire files into memory instead of streaming.

Retrying every error (validation errors shouldn't be retried).

Trade-offs and Alternatives

Strategies as functions (TS/Python/C++ lambdas) are lighter than class hierarchies when there's one method. Use classes when strategies carry configuration and several methods.

Real-World Use

Shop checkouts, notification services (Twilio/SendGrid integrations), parking management systems, trace-analysis tools for CAN logs (Vector ASC files).

Practice

Q1. (Easy) Add a FreeShippingOver rule to the cart.

Answer: Implement DiscountRule::discount returning the shipping fee when the subtotal exceeds a threshold, and register it — Cart doesn't change.

Q2. (Medium) How would you let some discounts stack and others not?

Answer: Give rules a stackable() flag or group them: sum all stackable discounts, take the max of exclusive ones, and apply the larger combination — documented and tested as a business rule.

Q3. (Advanced) Make the file pipeline process several files concurrently without mixing results.

Answer: Create one Pipeline (with its own parser state and error list) per file and run them in a thread/process pool; sinks must be thread-safe or each pipeline gets its own sink instances whose results are merged at the end.

Interview Question

Q1. "Design a notification system that supports new channels without code changes in the core."

Answer: Define a Channel interface (send(recipient, message)), implement adapters per provider, register them in a factory/registry (configuration-driven), and inject the registry into a dispatcher that resolves user preferences, applies retries with back-off for transient errors, rate-limits per channel, records delivery attempts, and falls back to the next preferred channel. Add an outbox/queue for durability at scale; keep templates/localization separate from transport.

Quick Quiz

Q1. Which pattern chooses the parser by format name?

Answer: Factory (registry) — with Strategy for the parsing behaviour.

Q2. Why no Truck subclass in the parking lot?

Answer: No behaviour differs by vehicle class; size is just data.

Q3. What guarantees the trace file is closed?

Answer: The context manager (with closing(...)).

Summary

Use strategies, factories and DI where variation is real; keep data as data.

Related: Modules 25, 28, 40, 43, 51.