Week 13 Tutorial: SOLID Principles
Problem 1 (Easy): Playlist Manager
A music app has a Playlist class. It keeps the songs, calculates total minutes, AND saves the playlist to a text file, AND prints a “share card” to send to friends. Too many jobs in one class — fix it.
Bad Code
class Playlist:
def __init__(self, name):
self.name = name
self.songs = [] # list of (title, minutes)
def add(self, title, minutes):
self.songs.append((title, minutes))
def total_minutes(self):
return sum(m for _, m in self.songs)
def save_to_file(self, filename):
with open(filename, "w") as f:
f.write(f"{self.name}\n")
for title, minutes in self.songs:
f.write(f"{title} - {minutes} min\n")
def print_share_card(self):
print(f"🎵 {self.name} ({self.total_minutes()} min)")
for title, _ in self.songs:
print(f" • {title}")
- Apply the Single Responsibility Principle (SRP).
- Identify the three different jobs this class does.
- Keep
Playlistresponsible only for storing songs and calculatingtotal_minutes(). Keep methodsadd(title, minutes)andtotal_minutes(). - Move file saving into a new class called
PlaylistFileWriterwith a methodsave(playlist, filename). - Move the share card printing into a new class called
PlaylistSharerwith a methodshare(playlist).
Usage
p = Playlist("Road Trip")
p.add("Highway Star", 6)
p.add("Born to Run", 5)
print(p.total_minutes())
PlaylistFileWriter().save(p, "trip.txt")
PlaylistSharer().share(p)
Expected output
11
🎵 Road Trip (11 min)
• Highway Star
• Born to Run
Problem 2 (Easy+): Parking Fee Calculator
A parking lot charges different hourly fees depending on the vehicle type. The current code uses a long if/elif chain. Every time the city adds a new vehicle type (scooter, bus, truck…), a developer has to reopen this class and edit it. Fix it.
Bad Code
class ParkingFeeCalculator:
def fee(self, vehicle_type, hours):
if vehicle_type == "bike":
return 1000 * hours
elif vehicle_type == "car":
return 3000 * hours
elif vehicle_type == "truck":
return 7000 * hours
else:
raise ValueError(f"unknown vehicle: {vehicle_type}")
- Apply the Open/Closed Principle (OCP).
- Replace the
if/elifchain with polymorphism, not a dictionary. - Create an abstract base class
Vehicle(useABCand@abstractmethod) with one abstract methodfee(hours). - Create three concrete classes:
Bike,Car,Truck. Each implementsfee(hours)with its own rate (1000, 3000, 7000 so’m/hour). - Rewrite
ParkingFeeCalculatorso its methodcalculate(vehicle: Vehicle, hours)simply delegates to the vehicle’sfee(). - Prove the principle works: add a new class
Scooter(rate = 500 so’m/hour) without editingParkingFeeCalculator,Bike,Car, orTruck.
Usage
calc = ParkingFeeCalculator()
print(calc.calculate(Bike(), 2))
print(calc.calculate(Car(), 3))
print(calc.calculate(Truck(), 1))
print(calc.calculate(Scooter(), 4))
Expected output
2000
9000
7000
2000
Problem 3 (Medium): Employee Payroll
A small company built a Payroll class. It calculates the monthly salary depending on the employee type (full-time, part-time, contractor) using an if/elif chain, AND it prints a payslip (a document showing an employee’s salary details), AND it “saves” the record to the database. Two principles are broken at once. Fix both.
Bad Code
class Payroll:
def __init__(self, name, emp_type, base):
self.name = name
self.emp_type = emp_type # "full_time" | "part_time" | "contractor"
self.base = base
def salary(self):
if self.emp_type == "full_time":
return self.base + 500_000 # fixed bonus
elif self.emp_type == "part_time":
return self.base * 0.5
elif self.emp_type == "contractor":
return self.base * 1.2 # higher rate, no benefits
else:
raise ValueError(f"unknown type: {self.emp_type}")
def print_payslip(self):
print(f"--- Payslip for {self.name} ---")
print(f"Type: {self.emp_type}")
print(f"Salary: {self.salary()} so'm")
def save_to_db(self):
print(f"INSERT INTO payroll VALUES ('{self.name}', {self.salary()})")
- Apply both SRP and OCP.
- OCP fix: create an abstract class
Employeewith an__init__method that acceptsnameandbase, and an abstractsalary()method. Then create three subclasses:FullTime,PartTime,Contractor. Each one implements its ownsalary()formula. - SRP fix: the
Employeeclasses must ONLY know how to compute salary. They must NOT print or save anything. - Move payslip printing into a separate class
PayslipPrinterwith methoddisplay(employee). - Move the database saving into a separate class
PayrollRepositorywith methodsave(employee). - Both helper classes must work with any subclass of
Employee. - Demonstrate OCP by adding a new class
Intern(salary =base * 0.3) without changing any existing class.
Usage
employees = [
FullTime("Aziz", 4_000_000),
PartTime("Malika", 3_000_000),
Contractor("Rustam", 5_000_000),
Intern("Dilnoza", 2_000_000),
]
printer = PayslipPrinter()
repo = PayrollRepository()
for e in employees:
printer.display(e)
repo.save(e)
Expected output
--- Payslip for Aziz ---
Salary: 4500000 so'm
INSERT INTO payroll VALUES ('Aziz', 4500000)
--- Payslip for Malika ---
Salary: 1500000.0 so'm
INSERT INTO payroll VALUES ('Malika', 1500000.0)
--- Payslip for Rustam ---
Salary: 6000000.0 so'm
INSERT INTO payroll VALUES ('Rustam', 6000000.0)
--- Payslip for Dilnoza ---
Salary: 600000.0 so'm
INSERT INTO payroll VALUES ('Dilnoza', 600000.0)
Problem 4 (Medium+): Newsletter Sender
A startup wrote a NewsletterSender that always gets subscribers from the real database and always sends through the real SMTP server. It works in production, but QA cannot test it safely because every test touches external systems. Fix the design so production code can use real services, while tests can use fake services.
Bad Code
class SmtpEmailServer:
def send(self, to, subject, body):
print(f"[SMTP → {to}] {subject}: {body}") # imagine real network call
class SubscriberDB:
def all_subscribers(self):
print("[DB] SELECT * FROM subscribers ...")
return ["aziz@example.com", "malika@example.com"]
class NewsletterSender:
def __init__(self):
self.email = SmtpEmailServer()
self.db = SubscriberDB()
def send_newsletter(self, subject, body):
for addr in self.db.all_subscribers():
personalized = f"Hello {addr.split('@')[0]}!\n\n{body}"
self.email.send(addr, subject, personalized)
- DIP problem:
NewsletterSendercreates low-level concrete objects (SmtpEmailServer,SubscriberDB) itself. - OCP problem: changing the subscriber source or mailer requires editing
NewsletterSenderinstead of plugging in a new implementation.
- Create an abstract class
SubscriberSourcewith one methodall_subscribers(). - Make
SubscriberDBimplementSubscriberSource. - Create an abstract class
Mailerwith one methodsend(to, subject, body). - Make
SmtpEmailServerimplementMailer. - Rewrite
NewsletterSender.__init__so it receivessubscribers: SubscriberSourceandmailer: Mailerfrom the outside. It must not createSubscriberDB()orSmtpEmailServer()itself. - Move the greeting logic into a
Personalizerclass with methodfor_address(address, body).NewsletterSendermay create a defaultPersonalizer, but it should also allow a custom one to be passed in. - Prove the design is testable by adding
FakeSubscribersandFakeMailer.FakeSubscribersshould return one hard-coded address.FakeMailershould print a fake send message and also append that message to a list.
Usage
# production wiring
sender = NewsletterSender(
subscribers=SubscriberDB(),
mailer=SmtpEmailServer(),
)
sender.send_newsletter("Weekly news", "Lots of updates this week.")
# test wiring — no network, no DB
mailer = FakeMailer()
NewsletterSender(FakeSubscribers(), mailer).send_newsletter("Hi", "body")
print(mailer.sent)
Expected output
[DB] SELECT * FROM subscribers ...
[SMTP → aziz@example.com] Weekly news: Hello aziz!
Lots of updates this week.
[SMTP → malika@example.com] Weekly news: Hello malika!
Lots of updates this week.
[FAKE → test@x.com] Hi: Hello test!
body
['[FAKE → test@x.com] Hi: Hello test!\n\nbody']