We start with a single class 'Employee' that handles both employee data and report generation. This violates the Single Responsibility Principle because it has more than one reason to change.
💡 Identifying a class with multiple responsibilities is the first step to applying SRP.
Line:class Employee:
def __init__(self, name, id):
self.name = name
self.id = id
def generate_report(self):
# generates report
💡 A class should have only one reason to change; here, 'Employee' mixes data and reporting.
compare
Identify Responsibilities in Employee Class
We analyze the 'Employee' class and identify two distinct responsibilities: managing employee data and generating reports.
💡 Separating concerns requires recognizing distinct responsibilities within a class.
💡 Delegation does not violate SRP if the class focuses on one reason to change.
compare
Verify Single Responsibility for ReportGenerator
We confirm that 'ReportGenerator' is solely responsible for report generation, with one reason to change.
💡 Each class should have a clear, single responsibility to minimize coupling and improve maintainability.
Line:# ReportGenerator responsibility:
# - Generate reports for employees
💡 Isolating responsibilities reduces complexity and improves code clarity.
reconstruct
Summary: SRP Achieved by Refactoring
The design now has two classes, each with a single responsibility: 'Employee' manages data, 'ReportGenerator' handles reporting. This design follows the Single Responsibility Principle.
💡 Refactoring to SRP improves code maintainability and reduces the risk of bugs when requirements change.
Line:# Final design:
# Employee: data management
# ReportGenerator: report generation
💡 SRP leads to cleaner, more modular, and easier to maintain code.
reconstruct
Read Final Answer: SRP Enforced
The final design clearly separates responsibilities, ensuring each class has one reason to change, fulfilling the Single Responsibility Principle.
💡 Understanding the final design confirms the effectiveness of SRP refactoring.
Line:# SRP enforced: Employee and ReportGenerator have distinct responsibilities
💡 The final class diagram is the answer to applying SRP correctly.
class Employee:
# STEP 1: Initial class with multiple responsibilities
def __init__(self, name, id): # STEP 1
self.name = name # STEP 1
self.id = id # STEP 1
# STEP 5: Add ReportGenerator instance
self.report_generator = ReportGenerator() # STEP 5
# STEP 6: Delegate report generation
def generate_report(self): # STEP 6
return self.report_generator.generate(self) # STEP 6
class ReportGenerator:
# STEP 3: New class for report generation
def generate(self, employee): # STEP 3
# generate report for employee
return f"Report for {employee.name} (ID: {employee.id})" # STEP 3
📊
Single Responsibility Principle - One Class, One Reason to Change - Watch the Algorithm Execute, Step by Step
Watching the step-by-step refactoring helps you understand why SRP matters and how to identify and separate responsibilities in class design.
Step 1/10
·Active fill★Answer cell
Initial design violates SRP by combining data and reporting.
Employee
−name: string
−id: string
+__init__()
+generate_report()
Highlighting responsibilities within the class.
Employee
−name: string
−id: string
+__init__()
+generate_report()
Introduced ReportGenerator class to separate concerns.
Employee
−name: string
−id: string
+__init__()
ReportGenerator
+generate()
ReportGenerator → Employee (1:1)
Removed report generation from Employee to enforce SRP.
Employee
−name: string
−id: string
+__init__()
ReportGenerator
+generate()
ReportGenerator → Employee (1:1)
Added composition to delegate report generation.
Employee
−name: string
−id: string
−report_generator: ReportGenerator
+__init__()
ReportGenerator
+generate()
Employee ◆ ReportGenerator (1:1)
Added delegation method to Employee.
Employee
−name: string
−id: string
−report_generator: ReportGenerator
+__init__()
+generate_report()
ReportGenerator
+generate()
Employee ◆ ReportGenerator (1:1)
Confirmed SRP adherence after refactoring.
Employee
−name: string
−id: string
−report_generator: ReportGenerator
+__init__()
+generate_report()
ReportGenerator
+generate()
Employee ◆ ReportGenerator (1:1)
Confirmed SRP adherence for ReportGenerator.
Employee
−name: string
−id: string
−report_generator: ReportGenerator
+__init__()
+generate_report()
ReportGenerator
+generate()
Employee ◆ ReportGenerator (1:1)
Final design respects Single Responsibility Principle.
Employee
−name: string
−id: string
−report_generator: ReportGenerator
+__init__()
+generate_report()
ReportGenerator
+generate()
Employee ◆ ReportGenerator (1:1)
SRP applied successfully; design is modular and maintainable.
Employee
−name: string
−id: string
−report_generator: ReportGenerator
+__init__()
+generate_report()
ReportGenerator
+generate()
Employee ◆ ReportGenerator (1:1)
Key Takeaways
✓ A class should have only one reason to change, which means it should have a single responsibility.
This insight is hard to see from code alone because multiple responsibilities can be hidden inside methods or fields.
✓ Separating responsibilities into different classes improves maintainability and reduces coupling.
Visualizing the separation helps understand how SRP leads to modular design.
✓ Delegation allows a class to expose functionality without violating SRP by forwarding tasks to dedicated classes.
Seeing delegation in the diagram clarifies how responsibilities remain separated while preserving interface usability.
Practice
(1/5)
1. Given the following Python code using the Builder pattern, what is the output of print(house) after constructing a basic house with the Director?
easy
A. House parts: Walls, Roof
B. House parts: Walls, Roof, Pool
C. House parts: Roof, Walls
D. House parts: Pool
Solution
Step 1: Trace Director.construct_basic_house()
The Director calls build_walls() and build_roof() on the builder, adding "Walls" and "Roof" to the house parts.
Step 2: Check the final house parts list
The house parts list contains ["Walls", "Roof"]. The pool is not added in this method.
Final Answer:
Option A -> Option A
Quick Check:
Only walls and roof are added in basic house construction [OK]
Hint: Basic house adds walls and roof only [OK]
Common Mistakes:
Assuming pool is added by default
Mixing order of parts
2. You need to ensure that a class has only one instance throughout the application lifecycle, and this instance must be lazily initialized in a thread-safe manner without incurring synchronization overhead on every access. Which design approach best fits this requirement?
easy
A. Use a simple static variable without synchronization and rely on the language's memory model.
B. Use a synchronized method that creates the instance on first call, locking every time.
C. Create the instance eagerly at class loading time to avoid synchronization issues.
D. Implement double-checked locking with a volatile instance variable to minimize synchronization overhead.
Solution
Step 1: Understand the problem constraints
The instance must be lazily initialized and thread-safe, but synchronization overhead should be minimized.
Step 2: Evaluate each approach
Synchronized method locks on every call, causing overhead. Eager initialization is thread-safe but not lazy. Unsynchronized static variable risks multiple instances in multithreaded contexts. Double-checked locking with volatile ensures lazy init, thread safety, and minimal locking.
Final Answer:
Option D -> Option D
Quick Check:
Double-checked locking balances thread safety and performance [OK]
3. Which of the following statements about using inheritance to model different vehicle types in the parking lot system is INCORRECT?
medium
A. Inheritance allows sharing common vehicle attributes and behaviors in a base Vehicle class
B. Using inheritance for vehicle types can lead to rigid designs that are hard to extend with new types
C. Inheritance is always the best approach to add new vehicle types without modifying existing code
D. Polymorphism enables treating all vehicle types uniformly when allocating parking spots
Solution
Step 1: Review inheritance benefits
Inheritance supports code reuse and polymorphism for uniform handling.
Step 2: Recognize limitations
Inheritance hierarchies can become rigid and hard to extend, violating open-closed principle.
Step 3: Identify incorrect statement
Claiming inheritance is always best ignores alternatives like composition or interfaces that improve extensibility.
Final Answer:
Option C -> Option C
Quick Check:
Inheritance is not always best for extensibility [OK]
Hint: Inheritance can cause rigidity; prefer composition for extensibility [OK]
Common Mistakes:
Assuming inheritance is always the best design choice
Ignoring polymorphism benefits
4. In a large-scale system where behaviors need to be dynamically changed at runtime, what is a key design consideration when using composition to avoid pitfalls that inheritance-based designs face?
hard
A. Use deep inheritance hierarchies to capture all behavior variations upfront.
B. Ensure that composed behavior objects are immutable to prevent side effects.
C. Avoid using interfaces and rely on concrete classes to reduce complexity.
D. Design behavior interfaces and use delegation so behaviors can be swapped without modifying the main object.
Solution
Step 1: Understand dynamic behavior change
To change behaviors at runtime, the design must support swapping behavior implementations easily.
Step 2: Analyze options
Ensure that composed behavior objects are immutable to prevent side effects. is not practical; immutability limits dynamic changes. Avoid using interfaces and rely on concrete classes to reduce complexity. increases coupling and reduces flexibility. Use deep inheritance hierarchies to capture all behavior variations upfront. contradicts favoring composition and leads to rigid designs.
Step 3: Confirm best practice
Design behavior interfaces and use delegation so behaviors can be swapped without modifying the main object. promotes defining behavior interfaces and delegating calls, enabling runtime swapping without modifying main objects.
Final Answer:
Option D -> Option D
Quick Check:
Delegation with interfaces is key to flexible, dynamic composition.
Hint: Use interfaces and delegation to swap behaviors dynamically, not inheritance or concrete classes.
Common Mistakes:
Thinking immutability aids dynamic behavior changes
Relying on inheritance for runtime flexibility
5. In a language that supports multiple inheritance with MRO, what happens if two parent classes define a method with the same name, but one parent class inherits from the other (forming a diamond), and the child class overrides that method? How does the MRO affect which method is called when the child class method calls super()?
hard
A. super() follows the MRO linearization, calling the next method in the MRO sequence, which may skip some classes.
B. super() calls the method from the immediate parent class only, ignoring the diamond structure.
C. super() calls all parent methods with the same name in parallel, combining their effects.
D. super() always calls the method from the base class at the top of the diamond first.
Solution
Step 1: Understand super() in multiple inheritance
super() does not simply call the immediate parent but follows the MRO linearization.
Step 2: MRO linearization
MRO creates a linear order of classes to avoid ambiguity and duplication, so super() calls the next method in this order.
Step 3: Eliminate incorrect options
super() calls the method from the immediate parent class only, ignoring the diamond structure is incorrect because super() is not limited to immediate parent. super() calls all parent methods with the same name in parallel, combining their effects is wrong because super() does not call methods in parallel. super() always calls the method from the base class at the top of the diamond first is incorrect because super() does not always start at the base class.