Single Responsibility Principle
Class recording
The Single Responsibility Principle
The Single Responsibility Principle is that functions should have a single responsibility—i.e., they should be cohesive. Group together into a function statements and logic that have a single, simple goal.
The Single Responsibility Principle is often stated as “each function has one clear reason to change.”
Benefits
- Each function has a clear purpose and answers one “what does this do?” question.
- Narrow behaviors are easier to unit test.
- A change to that single purpose only affects one function rather than requiring changes across unrelated code.
- Small, focused functions are easier to compose to solve bigger problems.
Red flags (violations):
- The function name needs “and” to describe it.
- It touches multiple domains (I/O, parsing, business rules, UI) at once.
- It has many parameters or returns mixed/compound results that signal different concerns.
- It has multiple try/except blocks for different activities.
- It both decides and acts (e.g., computes a result and prints/saves/sends it).
Example 1 - Decision logic mixed with formatting
Problematic code
def describe_temperature(celsius: float) -> str:
if celsius < 0:
color = "blue"
label = "Freezing"
elif celsius < 20:
color = "green"
label = "Cool"
else:
color = "red"
label = "Hot"
# Mixing presentation logic here
return f"{label} ({celsius}°C) shown in {color.upper()}"
Problem: This function both categorizes a temperature and formats a human-readable message. If we change color conventions or output format, unrelated logic breaks.
Fixed to separate out unrelated purposes
def classify_temperature(celsius: float) -> str:
if celsius < 0:
return "Freezing"
elif celsius < 20:
return "Cool"
return "Hot"
def color_for_temperature(label: str) -> str:
return {"Freezing": "blue", "Cool": "green", "Hot": "red"}[label]
def format_temperature_message(label: str, celsius: float, color: str) -> str:
return f"{label} ({celsius}°C) shown in {color.upper()}"
def describe_temperature(celsius: float) -> str:
label = classify_temperature(celsius)
color = color_for_temperature(label)
return format_temperature_message(label, celsius, color)
Why this is better: Each helper has a single reason to change — classification rules, color mapping, or formatting.
Example 2 - Data validation mixed with transformation
Problematic code
def normalize_user_input(data: dict) -> dict:
if "name" not in data or "email" not in data:
raise ValueError("Missing fields")
data["name"] = data["name"].strip().title()
data["email"] = data["email"].lower()
return data
Validation and transformation responsibilities are blended. Changing validation rules would risk altering transformation behavior.
Fixed by splitting the function
def validate_user_input(data: dict) -> None:
if "name" not in data or "email" not in data:
raise ValueError("Missing fields")
def normalize_user_fields(data: dict) -> dict:
return {
"name": data["name"].strip().title(),
"email": data["email"].lower(),
}
def process_user_input(data: dict) -> dict:
validate_user_input(data)
return normalize_user_fields(data)
Each function has one clear reason to change — validation rules vs. formatting rules.
How to refactor toward SRP
- Name first. Write a function name that states a single outcome; split if you need “and.”
- Separate concerns. Isolate I/O, parsing, validation, business rules, formatting, and presentation.
- Extract functions. Pull distinct blocks into helpers with clear inputs/outputs.
- Push side effects outward. Keep core logic pure; print/save at the edges.
Knowledge Check
- Which function best follows SRP?
process_and_save_and_print_order()compute_total(items, tax_rate)read_validate_compute()do_everything()
Answer
2 — the names of the others all imply that they have multiple responsibilities. - A common SRP smell is:
- One return statement
- Short parameter list
- A function that both validates input and writes files
- A pure function with docstring
Answer
3 — Validating input (from a user, from a file) and writing data to a file are distinct responsibilities within a program. - A good SRP-based refactor typically involves:
- Extracting cohesive operations into new functions.
- Reducing the number of function calls.
- Merging similar code into a single larger function.
- Avoiding helper functions.
Answer
1 — refactoring an SRP problem almost always will result in more functions in the program. - The SRP is violated when a function changes for more than one reason. “Reason” here refers to:
- Multiple developers editing the same code.
- Multiple sources of change tied to distinct responsibilities.
- The number of commits per week.
- The number of test cases.
Answer
2 — thinking of "responsibility" as "one task the program performs", if something about that task changes (e.g., getting input, validating input is correct, writing output) it should ideally only affect one function in the code that is separate from the other tasks.
Next up
Up next is the DRY principle and the Rule of Three.