Week 13 Assignment
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)} $")
- Create an abstract class
Memberthat acceptsnameand stores it, and has one abstract methodmonthly_fee(). - Create three concrete subclasses of
Member:Basic,Premium,Vip. Each implementsmonthly_fee()returning200_000,500_000, and1_000_000respectively. - Rewrite
GymManagerso it has ONLY amemberslist and a methodadd(member: Member). Remove every other method. - Create an abstract class
Storagewith one abstract methodsave(members). - Create a concrete class
FileStorage(Storage). Itssave(members)prints one line per member in the exact format[FILE] {name},{fee}. - Create an abstract class
Notifierwith one abstract methodnotify(members). - Create a concrete class
EmailNotifier(Notifier). Itsnotify(members)prints one line per member in the exact format[EMAIL → {name}] Your fee: {fee} $. - Add a method
runtoGymManagerthat 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)})")
- Create an abstract class
MenuItemthat acceptsnameand stores it, and has one abstract methodprice(). - Create three concrete subclasses of
MenuItem:Food,Drink,Dessert. Each implementsprice()returning50_000,20_000, and30_000respectively. - Rewrite
OrderSystemso it has ONLY anitemslist and a methodadd(item: MenuItem). Remove every other method. - Create an abstract class
Receiptwith one abstract methoddisplay(items). - Create a concrete class
ConsoleReceipt(Receipt). Itsdisplay(items)prints one line per item in the exact format{name}: {price}. - Create an abstract class
Repositorywith one abstract methodsave(items). - Create a concrete class
DbRepository(Repository). Itssave(items)prints one line per item in the exact formatINSERT INTO orders VALUES ('{name}', {price}). - Add a method
runtoOrderSystemthat 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")
- Create an abstract class
Roomthat acceptsguestand stores it, and has one abstract methodnightly_rate(). - Create three concrete subclasses of
Room:Single,Double,Suite. Each implementsnightly_rate()returning300_000,500_000, and1_200_000respectively. - Rewrite
HotelManagerso it has ONLY abookingslist and a methodbook(room: Room). Remove every other method. - Create an abstract class
Exporterwith one abstract methodexport(bookings). - Create a concrete class
CsvExporter(Exporter). Itsexport(bookings)prints one line per booking in the exact format{guest},{rate}. - Create an abstract class
Messengerwith one abstract methodnotify(bookings). - Create a concrete class
SmsMessenger(Messenger). Itsnotify(bookings)prints one line per booking in the exact format[SMS → {guest}] Your room is booked at {rate} €/night. - Add a method
runtoHotelManagerthat 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")
- Create an abstract class
Bookthat acceptsreaderand stores it, and has one abstract methodloan_days(). - Create three concrete subclasses of
Book:Novel,Textbook,Magazine. Each implementsloan_days()returning14,30, and7respectively. - Rewrite
LoanManagerso it has ONLY aloanslist and a methodadd(book: Book). Remove every other method. - Create an abstract class
Slipwith one abstract methodprint_slip(loans). - Create a concrete class
PaperSlip(Slip). Itsprint_slip(loans)prints one line per loan in the exact format{reader} -> {days} days. - Create an abstract class
Reminderwith one abstract methodsend(loans). - Create a concrete class
TelegramReminder(Reminder). Itssend(loans)prints one line per loan in the exact format[TG → {reader}] Return in {days} days. - Add a method
runtoLoanManagerthat 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)} ¥")
- Create an abstract class
Ridethat acceptspassengerand stores it, and has one abstract methodbase_fare(). - Create three concrete subclasses of
Ride:Economy,Comfort,Business. Each implementsbase_fare()returning15_000,25_000, and60_000respectively. - Rewrite
RideManagerso it has ONLY arideslist and a methodadd(ride: Ride). Remove every other method. - Create an abstract class
Logwith one abstract methodwrite(rides). - Create a concrete class
TextLog(Log). Itswrite(rides)prints one line per ride in the exact formatLOG: {passenger} | fare={fare}. - Create an abstract class
Notificationwith one abstract methodpush(rides). - Create a concrete class
PushNotification(Notification). Itspush(rides)prints one line per ride in the exact format[PUSH → {passenger}] Driver on the way. Fare {fare} ¥. - Add a method
runtoRideManagerthat 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)} £")
- Create an abstract class
Coursethat acceptsstudentand stores it, and has one abstract methodprice(). - Create three concrete subclasses of
Course:Beginner,Intermediate,Advanced. Each implementsprice()returning100_000,250_000, and500_000respectively. - Rewrite
CoursePlatformso it has ONLY anenrollmentslist and a methodenroll(course: Course). Remove every other method. - Create an abstract class
Reportwith one abstract methodgenerate(enrollments). - Create a concrete class
TextReport(Report). Itsgenerate(enrollments)prints one line per enrollment in the exact formatStudent: {student} | Tuition: {price}. - Create an abstract class
Mailerwith one abstract methodsend(enrollments). - Create a concrete class
EmailMailer(Mailer). Itssend(enrollments)prints one line per enrollment in the exact format[EMAIL → {student}] Enrolled. Pay {price} £. - Add a method
runtoCoursePlatformthat 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")
- Create an abstract class
Orderthat acceptscustomerand stores it, and has one abstract methodfee(). - Create three concrete subclasses of
Order:Near,City,Outskirts. Each implementsfee()returning5_000,12_000, and25_000respectively. - Rewrite
DeliveryServiceso it has ONLY adeliverieslist and a methodadd(order: Order). Remove every other method. - Create an abstract class
Archivewith one abstract methodstore(deliveries). - Create a concrete class
FileArchive(Archive). Itsstore(deliveries)prints one line per delivery in the exact formatARCHIVE | {customer} | {fee}. - Create an abstract class
Dispatcherwith one abstract methodcall(deliveries). - Create a concrete class
PhoneDispatcher(Dispatcher). Itscall(deliveries)prints one line per delivery in the exact format[CALL → {customer}] Courier dispatched, fee {fee} AUD. - Add a method
runtoDeliveryServicethat 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")
- Create an abstract class
Visitthat acceptspatientand stores it, and has one abstract methodcharge(). - Create three concrete subclasses of
Visit:Checkup,Surgery,Consult. Each implementscharge()returning80_000,2_000_000, and150_000respectively. - Rewrite
BillingSystemso it has ONLY avisitslist and a methodadd(visit: Visit). Remove every other method. - Create an abstract class
Invoicewith one abstract methodexport(visits). - Create a concrete class
TextInvoice(Invoice). Itsexport(visits)prints one line per visit in the exact formatINVOICE #{patient}: {amount} CAD. - Create an abstract class
Callerwith one abstract methodcall(visits). - Create a concrete class
PhoneCaller(Caller). Itscall(visits)prints one line per visit in the exact format[CALL → {patient}] Please pay {amount} CAD. - Add a method
runtoBillingSystemthat 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")
- Create an abstract class
Seatthat acceptsviewerand stores it, and has one abstract methodticket_price(). - Create three concrete subclasses of
Seat:Standard,Premium,Vip. Each implementsticket_price()returning35_000,70_000, and120_000respectively. - Rewrite
TicketSystemso it has ONLY abookingslist and a methodadd(seat: Seat). Remove every other method. - Create an abstract class
Ticketwith one abstract methodprint_ticket(bookings). - Create a concrete class
PaperTicket(Ticket). Itsprint_ticket(bookings)prints one line per booking in the exact formatTICKET <{viewer}> price={price}. - Create an abstract class
QrSenderwith one abstract methodsend(bookings). - Create a concrete class
TelegramQrSender(QrSender). Itssend(bookings)prints one line per booking in the exact format[QR → {viewer}] Show this at entrance. Paid {price} so'm. - Add a method
runtoTicketSystemthat 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")
- Create an abstract class
Packagethat acceptssubscriberand stores it, and has one abstract methodamount(). - Create three concrete subclasses of
Package:Small,Medium,Large. Each implementsamount()returning10_000,30_000, and70_000respectively. - Rewrite
TopupServiceso it has ONLY atopupslist and a methodadd(package: Package). Remove every other method. - Create an abstract class
Receiptwith one abstract methodwrite(topups). - Create a concrete class
TextReceipt(Receipt). Itswrite(topups)prints one line per top-up in the exact formatRECEIPT: {subscriber} +{amount}. - Create an abstract class
Confirmationwith one abstract methodconfirm(topups). - Create a concrete class
SmsConfirmation(Confirmation). Itsconfirm(topups)prints one line per top-up in the exact format[SMS → {subscriber}] Top-up of {amount} so'm successful. - Add a method
runtoTopupServicethat 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")
- Create an abstract class
Basketthat acceptsshopperand stores it, and has one abstract methodtotal(). - Create three concrete subclasses of
Basket:Mini,Family,Bulk. Each implementstotal()returning25_000,90_000, and250_000respectively. - Rewrite
CheckoutSystemso it has ONLY apurchaseslist and a methodadd(basket: Basket). Remove every other method. - Create an abstract class
Billwith one abstract methodprint_bill(purchases). - Create a concrete class
PaperBill(Bill). Itsprint_bill(purchases)prints one line per purchase in the exact formatBILL ({shopper}) = {total}. - Create an abstract class
Loyaltywith one abstract methodnotify(purchases). - Create a concrete class
EmailLoyalty(Loyalty). Itsnotify(purchases)prints one line per purchase in the exact format[LOYALTY → {shopper}] Earned points for {total} so'm. - Add a method
runtoCheckoutSystemthat 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")
- Create an abstract class
Parcelthat acceptssenderand stores it, and has one abstract methodcost(). - Create three concrete subclasses of
Parcel:Envelope,Box,Pallet. Each implementscost()returning8_000,22_000, and150_000respectively. - Rewrite
ShippingServiceso it has ONLY aparcelslist and a methodadd(parcel: Parcel). Remove every other method. - Create an abstract class
Labelwith one abstract methodstamp(parcels). - Create a concrete class
PaperLabel(Label). Itsstamp(parcels)prints one line per parcel in the exact formatLABEL [{sender}] cost: {cost}. - Create an abstract class
Warehousewith one abstract methodnotify(parcels). - Create a concrete class
RadioWarehouse(Warehouse). Itsnotify(parcels)prints one line per parcel in the exact format[WH → {sender}] Parcel ready, charge {cost} so'm. - Add a method
runtoShippingServicethat 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