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 nowinterface 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] ana@example.com: 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 foundimport 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: TrueStep-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.