← Computer Programming II

Variant 1: Gym Membership Manager

A gym uses one giant GymManager class. It stores members, knows every plan’s monthly fee through an if/elif chain, writes the member list to a file, and sends email reminders. Adding a new plan or switching to SMS reminders forces edits in the same class.

Bad Code

class GymManager:
    def __init__(self):
        self.members = []   # list of (name, plan)

    def add(self, name, plan):
        self.members.append((name, plan))

    def monthly_fee(self, plan):
        if plan == "basic":
            return 200_000
        elif plan == "premium":
            return 500_000
        elif plan == "vip":
            return 1_000_000
        else:
            raise ValueError(f"unknown plan: {plan}")

    def save_to_file(self):
        for name, plan in self.members:
            print(f"[FILE] {name},{self.monthly_fee(plan)}")

    def send_reminders(self):
        for name, plan in self.members:
            print(f"[EMAIL → {name}] Your fee: {self.monthly_fee(plan)} $")
  1. Create an abstract class Member that accepts name and stores it, and has one abstract method monthly_fee().
  2. Create three concrete subclasses of Member: Basic, Premium, Vip. Each implements monthly_fee() returning 200_000, 500_000, and 1_000_000 respectively.
  3. Rewrite GymManager so it has ONLY a members list and a method add(member: Member). Remove every other method.
  4. Create an abstract class Storage with one abstract method save(members).
  5. Create a concrete class FileStorage(Storage). Its save(members) prints one line per member in the exact format [FILE] {name},{fee}.
  6. Create an abstract class Notifier with one abstract method notify(members).
  7. Create a concrete class EmailNotifier(Notifier). Its notify(members) prints one line per member in the exact format [EMAIL → {name}] Your fee: {fee} $.
  8. Add a method run to GymManager that accepts a storage and a notifier, then saves members to storage and notifies members.

Usage

gym = GymManager()
gym.add(Basic("Frodo"))
gym.add(Premium("Aragorn"))
gym.add(Vip("Legolas"))

gym.run(FileStorage(), EmailNotifier())

Expected output

[FILE] Frodo,200000
[FILE] Aragorn,500000
[FILE] Legolas,1000000
[EMAIL → Frodo] Your fee: 200000 $
[EMAIL → Aragorn] Your fee: 500000 $
[EMAIL → Legolas] Your fee: 1000000 $

Variant 2: Restaurant Order System

A small restaurant wrote one big OrderSystem class. It stores ordered items, knows every item kind’s price through an if/elif chain, prints a receipt to the console, and saves orders to the database. Adding a new menu kind or printing receipts a different way means editing the same class every time.

Bad Code

class OrderSystem:
    def __init__(self):
        self.items = []   # list of (name, kind)

    def add(self, name, kind):
        self.items.append((name, kind))

    def price(self, kind):
        if kind == "food":
            return 50_000
        elif kind == "drink":
            return 20_000
        elif kind == "dessert":
            return 30_000
        else:
            raise ValueError(f"unknown kind: {kind}")

    def print_receipt(self):
        for name, kind in self.items:
            print(f"{name}: {self.price(kind)}")

    def save_to_db(self):
        for name, kind in self.items:
            print(f"INSERT INTO orders VALUES ('{name}', {self.price(kind)})")
  1. Create an abstract class MenuItem that accepts name and stores it, and has one abstract method price().
  2. Create three concrete subclasses of MenuItem: Food, Drink, Dessert. Each implements price() returning 50_000, 20_000, and 30_000 respectively.
  3. Rewrite OrderSystem so it has ONLY an items list and a method add(item: MenuItem). Remove every other method.
  4. Create an abstract class Receipt with one abstract method display(items).
  5. Create a concrete class ConsoleReceipt(Receipt). Its display(items) prints one line per item in the exact format {name}: {price}.
  6. Create an abstract class Repository with one abstract method save(items).
  7. Create a concrete class DbRepository(Repository). Its save(items) prints one line per item in the exact format INSERT INTO orders VALUES ('{name}', {price}).
  8. Add a method run to OrderSystem that accepts a receipt and a repository, then displays items on the receipt and saves items to the repository.

Usage

order = OrderSystem()
order.add(Food("Harry"))
order.add(Drink("Hermione"))
order.add(Dessert("Ron"))

order.run(ConsoleReceipt(), DbRepository())

Expected output

Harry: 50000
Hermione: 20000
Ron: 30000
INSERT INTO orders VALUES ('Harry', 50000)
INSERT INTO orders VALUES ('Hermione', 20000)
INSERT INTO orders VALUES ('Ron', 30000)

Variant 3: Hotel Booking Manager

A hotel built one big HotelManager class. It stores bookings, knows every room type’s nightly rate through an if/elif chain, exports bookings as CSV lines, and sends an SMS to each guest. Adding a new room type or switching to a different exporter forces edits in the same class.

Bad Code

class HotelManager:
    def __init__(self):
        self.bookings = []   # list of (guest, room_type)

    def book(self, guest, room_type):
        self.bookings.append((guest, room_type))

    def nightly_rate(self, room_type):
        if room_type == "single":
            return 300_000
        elif room_type == "double":
            return 500_000
        elif room_type == "suite":
            return 1_200_000
        else:
            raise ValueError(f"unknown room: {room_type}")

    def export_csv(self):
        for guest, rt in self.bookings:
            print(f"{guest},{self.nightly_rate(rt)}")

    def send_sms(self):
        for guest, rt in self.bookings:
            print(f"[SMS → {guest}] Your room is booked at {self.nightly_rate(rt)} €/night")
  1. Create an abstract class Room that accepts guest and stores it, and has one abstract method nightly_rate().
  2. Create three concrete subclasses of Room: Single, Double, Suite. Each implements nightly_rate() returning 300_000, 500_000, and 1_200_000 respectively.
  3. Rewrite HotelManager so it has ONLY a bookings list and a method book(room: Room). Remove every other method.
  4. Create an abstract class Exporter with one abstract method export(bookings).
  5. Create a concrete class CsvExporter(Exporter). Its export(bookings) prints one line per booking in the exact format {guest},{rate}.
  6. Create an abstract class Messenger with one abstract method notify(bookings).
  7. Create a concrete class SmsMessenger(Messenger). Its notify(bookings) prints one line per booking in the exact format [SMS → {guest}] Your room is booked at {rate} €/night.
  8. Add a method run to HotelManager that accepts an exporter and a messenger, then exports the bookings and notifies the guests.

Usage

hotel = HotelManager()
hotel.book(Single("Luke"))
hotel.book(Double("Leia"))
hotel.book(Suite("Han"))

hotel.run(CsvExporter(), SmsMessenger())

Expected output

Luke,300000
Leia,500000
Han,1200000
[SMS → Luke] Your room is booked at 300000 €/night
[SMS → Leia] Your room is booked at 500000 €/night
[SMS → Han] Your room is booked at 1200000 €/night

Variant 4: Library Loan Manager

A library wrote one big LoanManager class. It stores loans, knows every book kind’s loan duration through an if/elif chain, prints due slips, and sends Telegram reminders. Adding a new book kind or a different reminder channel forces edits in the same class.

Bad Code

class LoanManager:
    def __init__(self):
        self.loans = []   # list of (reader, kind)

    def add(self, reader, kind):
        self.loans.append((reader, kind))

    def loan_days(self, kind):
        if kind == "novel":
            return 14
        elif kind == "textbook":
            return 30
        elif kind == "magazine":
            return 7
        else:
            raise ValueError(f"unknown kind: {kind}")

    def print_slip(self):
        for reader, kind in self.loans:
            print(f"{reader} -> {self.loan_days(kind)} days")

    def send_reminder(self):
        for reader, kind in self.loans:
            print(f"[TG → {reader}] Return in {self.loan_days(kind)} days")
  1. Create an abstract class Book that accepts reader and stores it, and has one abstract method loan_days().
  2. Create three concrete subclasses of Book: Novel, Textbook, Magazine. Each implements loan_days() returning 14, 30, and 7 respectively.
  3. Rewrite LoanManager so it has ONLY a loans list and a method add(book: Book). Remove every other method.
  4. Create an abstract class Slip with one abstract method print_slip(loans).
  5. Create a concrete class PaperSlip(Slip). Its print_slip(loans) prints one line per loan in the exact format {reader} -> {days} days.
  6. Create an abstract class Reminder with one abstract method send(loans).
  7. Create a concrete class TelegramReminder(Reminder). Its send(loans) prints one line per loan in the exact format [TG → {reader}] Return in {days} days.
  8. Add a method run to LoanManager that accepts a slip and a reminder, then prints the slip for the loans and sends a reminder for the loans.

Usage

library = LoanManager()
library.add(Novel("Tony"))
library.add(Textbook("Steve"))
library.add(Magazine("Thor"))

library.run(PaperSlip(), TelegramReminder())

Expected output

Tony -> 14 days
Steve -> 30 days
Thor -> 7 days
[TG → Tony] Return in 14 days
[TG → Steve] Return in 30 days
[TG → Thor] Return in 7 days

Variant 5: Taxi Ride Manager

A taxi app wrote one big RideManager class. It stores rides, knows every ride tier’s base fare through an if/elif chain, writes a log line for each ride, and pushes a notification to the passenger. Adding a new tier or a different log destination forces edits in the same class.

Bad Code

class RideManager:
    def __init__(self):
        self.rides = []   # list of (passenger, tier)

    def add(self, passenger, tier):
        self.rides.append((passenger, tier))

    def base_fare(self, tier):
        if tier == "economy":
            return 15_000
        elif tier == "comfort":
            return 25_000
        elif tier == "business":
            return 60_000
        else:
            raise ValueError(f"unknown tier: {tier}")

    def write_log(self):
        for passenger, tier in self.rides:
            print(f"LOG: {passenger} | fare={self.base_fare(tier)}")

    def push_notification(self):
        for passenger, tier in self.rides:
            print(f"[PUSH → {passenger}] Driver on the way. Fare {self.base_fare(tier)} ¥")
  1. Create an abstract class Ride that accepts passenger and stores it, and has one abstract method base_fare().
  2. Create three concrete subclasses of Ride: Economy, Comfort, Business. Each implements base_fare() returning 15_000, 25_000, and 60_000 respectively.
  3. Rewrite RideManager so it has ONLY a rides list and a method add(ride: Ride). Remove every other method.
  4. Create an abstract class Log with one abstract method write(rides).
  5. Create a concrete class TextLog(Log). Its write(rides) prints one line per ride in the exact format LOG: {passenger} | fare={fare}.
  6. Create an abstract class Notification with one abstract method push(rides).
  7. Create a concrete class PushNotification(Notification). Its push(rides) prints one line per ride in the exact format [PUSH → {passenger}] Driver on the way. Fare {fare} ¥.
  8. Add a method run to RideManager that accepts a log and a notification, then writes the rides to the log and pushes a notification for the rides.

Usage

app = RideManager()
app.add(Economy("Naruto"))
app.add(Comfort("Sasuke"))
app.add(Business("Sakura"))

app.run(TextLog(), PushNotification())

Expected output

LOG: Naruto | fare=15000
LOG: Sasuke | fare=25000
LOG: Sakura | fare=60000
[PUSH → Naruto] Driver on the way. Fare 15000 ¥
[PUSH → Sasuke] Driver on the way. Fare 25000 ¥
[PUSH → Sakura] Driver on the way. Fare 60000 ¥

Variant 6: Online Course Platform

A learning startup wrote one big CoursePlatform class. It stores enrollments, knows every level’s tuition through an if/elif chain, generates a text report, and sends an enrollment email. Adding a new level or a different report format forces edits in the same class.

Bad Code

class CoursePlatform:
    def __init__(self):
        self.enrollments = []   # list of (student, level)

    def enroll(self, student, level):
        self.enrollments.append((student, level))

    def price(self, level):
        if level == "beginner":
            return 100_000
        elif level == "intermediate":
            return 250_000
        elif level == "advanced":
            return 500_000
        else:
            raise ValueError(f"unknown level: {level}")

    def make_report(self):
        for student, level in self.enrollments:
            print(f"Student: {student} | Tuition: {self.price(level)}")

    def send_email(self):
        for student, level in self.enrollments:
            print(f"[EMAIL → {student}] Enrolled. Pay {self.price(level)} £")
  1. Create an abstract class Course that accepts student and stores it, and has one abstract method price().
  2. Create three concrete subclasses of Course: Beginner, Intermediate, Advanced. Each implements price() returning 100_000, 250_000, and 500_000 respectively.
  3. Rewrite CoursePlatform so it has ONLY an enrollments list and a method enroll(course: Course). Remove every other method.
  4. Create an abstract class Report with one abstract method generate(enrollments).
  5. Create a concrete class TextReport(Report). Its generate(enrollments) prints one line per enrollment in the exact format Student: {student} | Tuition: {price}.
  6. Create an abstract class Mailer with one abstract method send(enrollments).
  7. Create a concrete class EmailMailer(Mailer). Its send(enrollments) prints one line per enrollment in the exact format [EMAIL → {student}] Enrolled. Pay {price} £.
  8. Add a method run to CoursePlatform that accepts a report and a mailer, then generates the report for the enrollments and sends an email for the enrollments.

Usage

platform = CoursePlatform()
platform.enroll(Beginner("Neo"))
platform.enroll(Intermediate("Trinity"))
platform.enroll(Advanced("Morpheus"))

platform.run(TextReport(), EmailMailer())

Expected output

Student: Neo | Tuition: 100000
Student: Trinity | Tuition: 250000
Student: Morpheus | Tuition: 500000
[EMAIL → Neo] Enrolled. Pay 100000 £
[EMAIL → Trinity] Enrolled. Pay 250000 £
[EMAIL → Morpheus] Enrolled. Pay 500000 £

Variant 7: Food Delivery Service

A food delivery startup wrote one big DeliveryService class. It stores deliveries, knows every zone’s delivery fee through an if/elif chain, archives each delivery, and calls a courier for it. Adding a new zone or a different archive destination forces edits in the same class.

Bad Code

class DeliveryService:
    def __init__(self):
        self.deliveries = []   # list of (customer, zone)

    def add(self, customer, zone):
        self.deliveries.append((customer, zone))

    def fee(self, zone):
        if zone == "near":
            return 5_000
        elif zone == "city":
            return 12_000
        elif zone == "outskirts":
            return 25_000
        else:
            raise ValueError(f"unknown zone: {zone}")

    def archive(self):
        for customer, zone in self.deliveries:
            print(f"ARCHIVE | {customer} | {self.fee(zone)}")

    def call_courier(self):
        for customer, zone in self.deliveries:
            print(f"[CALL → {customer}] Courier dispatched, fee {self.fee(zone)} AUD")
  1. Create an abstract class Order that accepts customer and stores it, and has one abstract method fee().
  2. Create three concrete subclasses of Order: Near, City, Outskirts. Each implements fee() returning 5_000, 12_000, and 25_000 respectively.
  3. Rewrite DeliveryService so it has ONLY a deliveries list and a method add(order: Order). Remove every other method.
  4. Create an abstract class Archive with one abstract method store(deliveries).
  5. Create a concrete class FileArchive(Archive). Its store(deliveries) prints one line per delivery in the exact format ARCHIVE | {customer} | {fee}.
  6. Create an abstract class Dispatcher with one abstract method call(deliveries).
  7. Create a concrete class PhoneDispatcher(Dispatcher). Its call(deliveries) prints one line per delivery in the exact format [CALL → {customer}] Courier dispatched, fee {fee} AUD.
  8. Add a method run to DeliveryService that accepts an archive and a dispatcher, then stores the deliveries in the archive and calls a courier for the deliveries.

Usage

service = DeliveryService()
service.add(Near("Sam"))
service.add(City("Gimli"))
service.add(Outskirts("Gandalf"))

service.run(FileArchive(), PhoneDispatcher())

Expected output

ARCHIVE | Sam | 5000
ARCHIVE | Gimli | 12000
ARCHIVE | Gandalf | 25000
[CALL → Sam] Courier dispatched, fee 5000 AUD
[CALL → Gimli] Courier dispatched, fee 12000 AUD
[CALL → Gandalf] Courier dispatched, fee 25000 AUD

Variant 8: Hospital Billing System

A hospital wrote one big BillingSystem class. It stores patient visits, knows every visit type’s charge through an if/elif chain, exports invoices, and calls each patient. Adding a new visit type or a different invoice format forces edits in the same class.

Bad Code

class BillingSystem:
    def __init__(self):
        self.visits = []   # list of (patient, visit_type)

    def add(self, patient, visit_type):
        self.visits.append((patient, visit_type))

    def charge(self, visit_type):
        if visit_type == "checkup":
            return 80_000
        elif visit_type == "surgery":
            return 2_000_000
        elif visit_type == "consult":
            return 150_000
        else:
            raise ValueError(f"unknown visit: {visit_type}")

    def export_invoice(self):
        for patient, vt in self.visits:
            print(f"INVOICE #{patient}: {self.charge(vt)} CAD")

    def call_patient(self):
        for patient, vt in self.visits:
            print(f"[CALL → {patient}] Please pay {self.charge(vt)} CAD")
  1. Create an abstract class Visit that accepts patient and stores it, and has one abstract method charge().
  2. Create three concrete subclasses of Visit: Checkup, Surgery, Consult. Each implements charge() returning 80_000, 2_000_000, and 150_000 respectively.
  3. Rewrite BillingSystem so it has ONLY a visits list and a method add(visit: Visit). Remove every other method.
  4. Create an abstract class Invoice with one abstract method export(visits).
  5. Create a concrete class TextInvoice(Invoice). Its export(visits) prints one line per visit in the exact format INVOICE #{patient}: {amount} CAD.
  6. Create an abstract class Caller with one abstract method call(visits).
  7. Create a concrete class PhoneCaller(Caller). Its call(visits) prints one line per visit in the exact format [CALL → {patient}] Please pay {amount} CAD.
  8. Add a method run to BillingSystem that accepts an invoice and a caller, then exports the invoice for the visits and calls each patient for the visits.

Usage

hospital = BillingSystem()
hospital.add(Checkup("Albus"))
hospital.add(Surgery("Severus"))
hospital.add(Consult("Draco"))

hospital.run(TextInvoice(), PhoneCaller())

Expected output

INVOICE #Albus: 80000 CAD
INVOICE #Severus: 2000000 CAD
INVOICE #Draco: 150000 CAD
[CALL → Albus] Please pay 80000 CAD
[CALL → Severus] Please pay 2000000 CAD
[CALL → Draco] Please pay 150000 CAD

Variant 9: Cinema Ticket System

A cinema wrote one big TicketSystem class. It stores bookings, knows every seat class’s ticket price through an if/elif chain, prints paper tickets, and sends a QR code to the viewer. Adding a new seat class or a different delivery channel forces edits in the same class.

Bad Code

class TicketSystem:
    def __init__(self):
        self.bookings = []   # list of (viewer, seat_class)

    def add(self, viewer, seat_class):
        self.bookings.append((viewer, seat_class))

    def ticket_price(self, seat_class):
        if seat_class == "standard":
            return 35_000
        elif seat_class == "premium":
            return 70_000
        elif seat_class == "vip":
            return 120_000
        else:
            raise ValueError(f"unknown seat: {seat_class}")

    def print_ticket(self):
        for viewer, sc in self.bookings:
            print(f"TICKET <{viewer}> price={self.ticket_price(sc)}")

    def send_qr(self):
        for viewer, sc in self.bookings:
            print(f"[QR → {viewer}] Show this at entrance. Paid {self.ticket_price(sc)} CHF")
  1. Create an abstract class Seat that accepts viewer and stores it, and has one abstract method ticket_price().
  2. Create three concrete subclasses of Seat: Standard, Premium, Vip. Each implements ticket_price() returning 35_000, 70_000, and 120_000 respectively.
  3. Rewrite TicketSystem so it has ONLY a bookings list and a method add(seat: Seat). Remove every other method.
  4. Create an abstract class Ticket with one abstract method print_ticket(bookings).
  5. Create a concrete class PaperTicket(Ticket). Its print_ticket(bookings) prints one line per booking in the exact format TICKET <{viewer}> price={price}.
  6. Create an abstract class QrSender with one abstract method send(bookings).
  7. Create a concrete class TelegramQrSender(QrSender). Its send(bookings) prints one line per booking in the exact format [QR → {viewer}] Show this at entrance. Paid {price} so'm.
  8. Add a method run to TicketSystem that accepts a ticket and a QR sender, then prints the ticket for the bookings and sends a QR code for the bookings.

Usage

cinema = TicketSystem()
cinema.add(Standard("Anakin"))
cinema.add(Premium("Obi-Wan"))
cinema.add(Vip("Yoda"))

cinema.run(PaperTicket(), TelegramQrSender())

Expected output

TICKET <Anakin> price=35000
TICKET <Obi-Wan> price=70000
TICKET <Yoda> price=120000
[QR → Anakin] Show this at entrance. Paid 35000 so'm
[QR → Obi-Wan] Show this at entrance. Paid 70000 so'm
[QR → Yoda] Show this at entrance. Paid 120000 so'm

Variant 10: Mobile Top-Up Service

A mobile carrier wrote one big TopupService class. It stores top-ups, knows every package’s amount through an if/elif chain, writes a receipt for each top-up, and sends an SMS confirmation. Adding a new package or a different confirmation channel forces edits in the same class.

Bad Code

class TopupService:
    def __init__(self):
        self.topups = []   # list of (subscriber, package)

    def add(self, subscriber, package):
        self.topups.append((subscriber, package))

    def amount(self, package):
        if package == "small":
            return 10_000
        elif package == "medium":
            return 30_000
        elif package == "large":
            return 70_000
        else:
            raise ValueError(f"unknown package: {package}")

    def write_receipt(self):
        for subscriber, pkg in self.topups:
            print(f"RECEIPT: {subscriber} +{self.amount(pkg)}")

    def confirm_sms(self):
        for subscriber, pkg in self.topups:
            print(f"[SMS → {subscriber}] Top-up of {self.amount(pkg)} so'm successful")
  1. Create an abstract class Package that accepts subscriber and stores it, and has one abstract method amount().
  2. Create three concrete subclasses of Package: Small, Medium, Large. Each implements amount() returning 10_000, 30_000, and 70_000 respectively.
  3. Rewrite TopupService so it has ONLY a topups list and a method add(package: Package). Remove every other method.
  4. Create an abstract class Receipt with one abstract method write(topups).
  5. Create a concrete class TextReceipt(Receipt). Its write(topups) prints one line per top-up in the exact format RECEIPT: {subscriber} +{amount}.
  6. Create an abstract class Confirmation with one abstract method confirm(topups).
  7. Create a concrete class SmsConfirmation(Confirmation). Its confirm(topups) prints one line per top-up in the exact format [SMS → {subscriber}] Top-up of {amount} so'm successful.
  8. Add a method run to TopupService that accepts a receipt and a confirmation, then writes the receipt for the top-ups and confirms the top-ups.

Usage

carrier = TopupService()
carrier.add(Small("Peter"))
carrier.add(Medium("Natasha"))
carrier.add(Large("Bruce"))

carrier.run(TextReceipt(), SmsConfirmation())

Expected output

RECEIPT: Peter +10000
RECEIPT: Natasha +30000
RECEIPT: Bruce +70000
[SMS → Peter] Top-up of 10000 so'm successful
[SMS → Natasha] Top-up of 30000 so'm successful
[SMS → Bruce] Top-up of 70000 so'm successful

Variant 11: Grocery Checkout System

A supermarket wrote one big CheckoutSystem class. It stores purchases, knows every basket type’s total through an if/elif chain, prints a paper bill, and emails loyalty points. Adding a new basket type or a different loyalty channel forces edits in the same class.

Bad Code

class CheckoutSystem:
    def __init__(self):
        self.purchases = []   # list of (shopper, basket)

    def add(self, shopper, basket):
        self.purchases.append((shopper, basket))

    def total(self, basket):
        if basket == "mini":
            return 25_000
        elif basket == "family":
            return 90_000
        elif basket == "bulk":
            return 250_000
        else:
            raise ValueError(f"unknown basket: {basket}")

    def print_bill(self):
        for shopper, basket in self.purchases:
            print(f"BILL ({shopper}) = {self.total(basket)}")

    def email_loyalty(self):
        for shopper, basket in self.purchases:
            print(f"[LOYALTY → {shopper}] Earned points for {self.total(basket)} so'm")
  1. Create an abstract class Basket that accepts shopper and stores it, and has one abstract method total().
  2. Create three concrete subclasses of Basket: Mini, Family, Bulk. Each implements total() returning 25_000, 90_000, and 250_000 respectively.
  3. Rewrite CheckoutSystem so it has ONLY a purchases list and a method add(basket: Basket). Remove every other method.
  4. Create an abstract class Bill with one abstract method print_bill(purchases).
  5. Create a concrete class PaperBill(Bill). Its print_bill(purchases) prints one line per purchase in the exact format BILL ({shopper}) = {total}.
  6. Create an abstract class Loyalty with one abstract method notify(purchases).
  7. Create a concrete class EmailLoyalty(Loyalty). Its notify(purchases) prints one line per purchase in the exact format [LOYALTY → {shopper}] Earned points for {total} so'm.
  8. Add a method run to CheckoutSystem that accepts a bill and a loyalty, then prints the bill for the purchases and notifies loyalty for the purchases.

Usage

store = CheckoutSystem()
store.add(Mini("Kakashi"))
store.add(Family("Itachi"))
store.add(Bulk("Hinata"))

store.run(PaperBill(), EmailLoyalty())

Expected output

BILL (Kakashi) = 25000
BILL (Itachi) = 90000
BILL (Hinata) = 250000
[LOYALTY → Kakashi] Earned points for 25000 so'm
[LOYALTY → Itachi] Earned points for 90000 so'm
[LOYALTY → Hinata] Earned points for 250000 so'm

Variant 12: Parcel Shipping Service

A post office wrote one big ShippingService class. It stores parcels, knows every parcel size’s cost through an if/elif chain, stamps a label for each parcel, and notifies the warehouse. Adding a new parcel size or a different warehouse channel forces edits in the same class.

Bad Code

class ShippingService:
    def __init__(self):
        self.parcels = []   # list of (sender, size)

    def add(self, sender, size):
        self.parcels.append((sender, size))

    def cost(self, size):
        if size == "envelope":
            return 8_000
        elif size == "box":
            return 22_000
        elif size == "pallet":
            return 150_000
        else:
            raise ValueError(f"unknown size: {size}")

    def stamp_label(self):
        for sender, sz in self.parcels:
            print(f"LABEL [{sender}] cost: {self.cost(sz)}")

    def notify_warehouse(self):
        for sender, sz in self.parcels:
            print(f"[WH → {sender}] Parcel ready, charge {self.cost(sz)} so'm")
  1. Create an abstract class Parcel that accepts sender and stores it, and has one abstract method cost().
  2. Create three concrete subclasses of Parcel: Envelope, Box, Pallet. Each implements cost() returning 8_000, 22_000, and 150_000 respectively.
  3. Rewrite ShippingService so it has ONLY a parcels list and a method add(parcel: Parcel). Remove every other method.
  4. Create an abstract class Label with one abstract method stamp(parcels).
  5. Create a concrete class PaperLabel(Label). Its stamp(parcels) prints one line per parcel in the exact format LABEL [{sender}] cost: {cost}.
  6. Create an abstract class Warehouse with one abstract method notify(parcels).
  7. Create a concrete class RadioWarehouse(Warehouse). Its notify(parcels) prints one line per parcel in the exact format [WH → {sender}] Parcel ready, charge {cost} so'm.
  8. Add a method run to ShippingService that accepts a label and a warehouse, then stamps a label for the parcels and notifies the warehouse for the parcels.

Usage

post = ShippingService()
post.add(Envelope("Cypher"))
post.add(Box("Oracle"))
post.add(Pallet("Smith"))

post.run(PaperLabel(), RadioWarehouse())

Expected output

LABEL [Cypher] cost: 8000
LABEL [Oracle] cost: 22000
LABEL [Smith] cost: 150000
[WH → Cypher] Parcel ready, charge 8000 so'm
[WH → Oracle] Parcel ready, charge 22000 so'm
[WH → Smith] Parcel ready, charge 150000 so'm