Assign Responsibilities
Introduction: Why Responsibilities Matter
Section titled “Introduction: Why Responsibilities Matter”Assigning responsibilities correctly is crucial for creating maintainable, testable, and scalable code. Poor responsibility assignment leads to god classes, tight coupling, and code that’s hard to understand and modify.
Visual: The Problem
Section titled “Visual: The Problem”Part 1: Understanding Responsibilities
Section titled “Part 1: Understanding Responsibilities”What Are Responsibilities?
Section titled “What Are Responsibilities?”A responsibility is a reason for a class to change. Each class should have one primary responsibility - one reason to exist and one reason to change.
Types of Responsibilities
Section titled “Types of Responsibilities”- Data Management - Storing and managing data
- Business Logic - Implementing business rules
- Coordination - Coordinating between other classes
- Validation - Validating inputs and states
- Communication - Interacting with external systems
Visual: Responsibility Types
Section titled “Visual: Responsibility Types”Definition: A class should have only one reason to change.
What this means:
- Each class should do one thing and do it well
- If a class has multiple reasons to change, split it
- Changes to one responsibility shouldn’t affect others
Visual: SRP in Action
Section titled “Visual: SRP in Action”Part 2: How to Assign Responsibilities
Section titled “Part 2: How to Assign Responsibilities”Step-by-Step Process
Section titled “Step-by-Step Process”- Identify what each entity needs to do
- Group related operations
- Assign to appropriate class
- Ensure single responsibility
- Check for cohesion
Example 1: Library Management System
Section titled “Example 1: Library Management System”Entities Identified:
- Book
- Member
- Loan
- Reservation
- Fine
Let’s assign responsibilities:
Book Responsibilities
Section titled “Book Responsibilities”What does a Book need to do?
- Store book information (title, author, ISBN)
- Track availability status
- Know its location in library
Book Responsibilities:
class Book: # Data Management def __init__(self, isbn: str, title: str, author: str): self.isbn = isbn self.title = title self.author = author self.is_available = True
# State Management def mark_as_borrowed(self) -> None: self.is_available = False
def mark_as_available(self) -> None: self.is_available = True
def is_available(self) -> bool: return self.is_availableclass Book { private String isbn; private String title; private String author; private boolean isAvailable;
// Data Management public Book(String isbn, String title, String author) { this.isbn = isbn; this.title = title; this.author = author; this.isAvailable = true; }
// State Management public void markAsBorrowed() { this.isAvailable = false; }
public void markAsAvailable() { this.isAvailable = true; }
public boolean isAvailable() { return this.isAvailable; }}Responsibility: Manage book data and availability state
Loan Responsibilities
Section titled “Loan Responsibilities”What does a Loan need to do?
- Store loan information (book, member, dates)
- Calculate due date
- Check if overdue
- Calculate fine amount
Loan Responsibilities:
class Loan: def __init__(self, book: Book, member: Member, loan_date: datetime): self.book = book self.member = member self.loan_date = loan_date self.due_date = self._calculate_due_date() self.return_date = None
# Business Logic def _calculate_due_date(self) -> datetime: return self.loan_date + timedelta(days=14)
def is_overdue(self) -> bool: if self.return_date: return False return datetime.now() > self.due_date
def calculate_fine(self) -> float: if not self.is_overdue(): return 0.0 days_overdue = (datetime.now() - self.due_date).days return days_overdue * 1.0 # $1 per day
# State Management def return_book(self) -> None: self.return_date = datetime.now()class Loan { private Book book; private Member member; private LocalDateTime loanDate; private LocalDateTime dueDate; private LocalDateTime returnDate;
public Loan(Book book, Member member, LocalDateTime loanDate) { this.book = book; this.member = member; this.loanDate = loanDate; this.dueDate = calculateDueDate(); this.returnDate = null; }
// Business Logic private LocalDateTime calculateDueDate() { return this.loanDate.plusDays(14); }
public boolean isOverdue() { if (this.returnDate != null) { return false; } return LocalDateTime.now().isAfter(this.dueDate); }
public double calculateFine() { if (!isOverdue()) { return 0.0; } long daysOverdue = ChronoUnit.DAYS.between(this.dueDate, LocalDateTime.now()); return daysOverdue * 1.0; // $1 per day }
// State Management public void returnBook() { this.returnDate = LocalDateTime.now(); }}Responsibility: Manage loan lifecycle and calculate fines
Visual: Responsibility Assignment
Section titled “Visual: Responsibility Assignment”classDiagram
direction LR
class Book {
+StoreBookData()
+TrackAvailability()
+ManageState()
}
class Loan {
+StoreLoanData()
+CalculateDueDate()
+CheckOverdue()
+CalculateFine()
}
class Member {
+StoreMemberData()
+TrackMembership()
+ValidateEligibility()
}
class Library {
+ManageBooks()
+ProcessLoans()
+CoordinateOperations()
}
Part 3: Common Responsibility Patterns
Section titled “Part 3: Common Responsibility Patterns”Pattern 1: Data Holder
Section titled “Pattern 1: Data Holder”Responsibility: Store and manage data
Example:
class Member: def __init__(self, member_id: str, name: str, email: str): self.member_id = member_id self.name = name self.email = email self.membership_type = "STANDARD"
def update_membership(self, new_type: str) -> None: self.membership_type = new_typeclass Member { private String memberId; private String name; private String email; private String membershipType;
public Member(String memberId, String name, String email) { this.memberId = memberId; this.name = name; this.email = email; this.membershipType = "STANDARD"; }
public void updateMembership(String newType) { this.membershipType = newType; }}When to use: When you need to store data with minimal logic
Pattern 2: Business Logic Handler
Section titled “Pattern 2: Business Logic Handler”Responsibility: Implement business rules and calculations
Example:
class FineCalculator: def calculate_fine(self, loan: Loan) -> float: if not loan.is_overdue(): return 0.0
days_overdue = (datetime.now() - loan.due_date).days
# Business rule: $1 per day for first 7 days, $2 per day after if days_overdue <= 7: return days_overdue * 1.0 else: return 7.0 + (days_overdue - 7) * 2.0class FineCalculator { public double calculateFine(Loan loan) { if (!loan.isOverdue()) { return 0.0; }
long daysOverdue = ChronoUnit.DAYS.between(loan.getDueDate(), LocalDateTime.now());
// Business rule: $1 per day for first 7 days, $2 per day after if (daysOverdue <= 7) { return daysOverdue * 1.0; } else { return 7.0 + (daysOverdue - 7) * 2.0; } }}When to use: When you have complex business rules
Pattern 3: Coordinator/Orchestrator
Section titled “Pattern 3: Coordinator/Orchestrator”Responsibility: Coordinate between multiple classes
Example:
class LibraryService: def __init__(self, book_repository: BookRepository, loan_repository: LoanRepository, fine_calculator: FineCalculator): self.book_repository = book_repository self.loan_repository = loan_repository self.fine_calculator = fine_calculator
def borrow_book(self, book_id: str, member_id: str) -> Loan: # Coordinate between multiple classes book = self.book_repository.find_by_id(book_id) member = self.member_repository.find_by_id(member_id)
if not book.is_available(): raise ValueError("Book not available")
loan = Loan(book, member, datetime.now()) self.loan_repository.save(loan) book.mark_as_borrowed()
return loanclass LibraryService { private BookRepository bookRepository; private LoanRepository loanRepository; private FineCalculator fineCalculator;
public LibraryService(BookRepository bookRepository, LoanRepository loanRepository, FineCalculator fineCalculator) { this.bookRepository = bookRepository; this.loanRepository = loanRepository; this.fineCalculator = fineCalculator; }
public Loan borrowBook(String bookId, String memberId) { // Coordinate between multiple classes Book book = bookRepository.findById(bookId); Member member = memberRepository.findById(memberId);
if (!book.isAvailable()) { throw new IllegalStateException("Book not available"); }
Loan loan = new Loan(book, member, LocalDateTime.now()); loanRepository.save(loan); book.markAsBorrowed();
return loan; }}When to use: When you need to coordinate multiple operations
Pattern 4: Validator
Section titled “Pattern 4: Validator”Responsibility: Validate inputs and states
Example:
class LoanValidator: def validate_loan(self, book: Book, member: Member) -> bool: if not book.is_available(): raise ValueError("Book is not available")
if member.has_overdue_books(): raise ValueError("Member has overdue books")
if member.get_active_loans_count() >= 5: raise ValueError("Member has reached loan limit")
return Trueclass LoanValidator { public boolean validateLoan(Book book, Member member) { if (!book.isAvailable()) { throw new IllegalStateException("Book is not available"); }
if (member.hasOverdueBooks()) { throw new IllegalStateException("Member has overdue books"); }
if (member.getActiveLoansCount() >= 5) { throw new IllegalStateException("Member has reached loan limit"); }
return true; }}When to use: When you need to validate before operations
Pattern 5: Repository/Data Access
Section titled “Pattern 5: Repository/Data Access”Responsibility: Handle data persistence
Example:
class BookRepository: def save(self, book: Book) -> None: # Save to database pass
def find_by_id(self, book_id: str) -> Book: # Retrieve from database pass
def find_available_books(self) -> List[Book]: # Query database passclass BookRepository { public void save(Book book) { // Save to database }
public Book findById(String bookId) { // Retrieve from database return null; }
public List<Book> findAvailableBooks() { // Query database return new ArrayList<>(); }}When to use: When you need to abstract data access
Part 4: Common Mistakes and How to Avoid Them
Section titled “Part 4: Common Mistakes and How to Avoid Them”Mistake 1: God Class (Too Many Responsibilities)
Section titled “Mistake 1: God Class (Too Many Responsibilities)”Bad Example:
class LibraryManager: # Too many responsibilities! def add_book(self, book: Book): pass def remove_book(self, book_id: str): pass def borrow_book(self, book_id: str, member_id: str): pass def return_book(self, loan_id: str): pass def calculate_fine(self, loan_id: str): float: pass def send_notification(self, member_id: str, message: str): pass def generate_report(self) -> Report: pass def process_payment(self, member_id: str, amount: float): passclass LibraryManager { // Too many responsibilities! public void addBook(Book book) {} public void removeBook(String bookId) {} public void borrowBook(String bookId, String memberId) {} public void returnBook(String loanId) {} public double calculateFine(String loanId) { return 0.0; } public void sendNotification(String memberId, String message) {} public Report generateReport() { return null; } public void processPayment(String memberId, double amount) {}}Problems:
- Too many reasons to change
- Hard to test
- Hard to maintain
- Violates SRP
Good Example:
class BookService: def add_book(self, book: Book): pass def remove_book(self, book_id: str): pass
class LoanService: def borrow_book(self, book_id: str, member_id: str): pass def return_book(self, loan_id: str): pass
class FineService: def calculate_fine(self, loan_id: str) -> float: pass
class NotificationService: def send_notification(self, member_id: str, message: str): pass
class ReportService: def generate_report(self) -> Report: pass
class PaymentService: def process_payment(self, member_id: str, amount: float): passclass BookService { public void addBook(Book book) {} public void removeBook(String bookId) {}}
class LoanService { public void borrowBook(String bookId, String memberId) {} public void returnBook(String loanId) {}}
class FineService { public double calculateFine(String loanId) { return 0.0; }}
class NotificationService { public void sendNotification(String memberId, String message) {}}
class ReportService { public Report generateReport() { return null; }}
class PaymentService { public void processPayment(String memberId, double amount) {}}Visual: God Class vs Good Design
Section titled “Visual: God Class vs Good Design”Mistake 2: Anemic Domain Model (Too Few Responsibilities)
Section titled “Mistake 2: Anemic Domain Model (Too Few Responsibilities)”Bad Example:
class Book: # Only data, no behavior! def __init__(self, isbn: str, title: str): self.isbn = isbn self.title = title self.is_available = True
class BookManager: # All logic in manager def mark_as_borrowed(self, book: Book): book.is_available = False
def mark_as_available(self, book: Book): book.is_available = Trueclass Book { // Only data, no behavior! public String isbn; public String title; public boolean isAvailable;
public Book(String isbn, String title) { this.isbn = isbn; this.title = title; this.isAvailable = true; }}
class BookManager { // All logic in manager public void markAsBorrowed(Book book) { book.isAvailable = false; }
public void markAsAvailable(Book book) { book.isAvailable = true; }}Problems:
- Data and behavior separated
- Objects are just data containers
- Logic scattered across managers
Good Example:
class Book: def __init__(self, isbn: str, title: str): self.isbn = isbn self.title = title self._is_available = True
# Behavior with data def mark_as_borrowed(self) -> None: self._is_available = False
def mark_as_available(self) -> None: self._is_available = True
def is_available(self) -> bool: return self._is_availableclass Book { private String isbn; private String title; private boolean isAvailable;
public Book(String isbn, String title) { this.isbn = isbn; this.title = title; this.isAvailable = true; }
// Behavior with data public void markAsBorrowed() { this.isAvailable = false; }
public void markAsAvailable() { this.isAvailable = true; }
public boolean isAvailable() { return this.isAvailable; }}Mistake 3: Wrong Class for Responsibility
Section titled “Mistake 3: Wrong Class for Responsibility”Bad Example:
class Book: def send_overdue_notification(self, member: Member): # Book shouldn't send notifications! email_service.send(member.email, "Book is overdue")class Book { public void sendOverdueNotification(Member member) { // Book shouldn't send notifications! emailService.send(member.getEmail(), "Book is overdue"); }}Problem: Book is responsible for sending notifications (should be NotificationService)
Good Example:
class Book: def is_overdue(self) -> bool: return datetime.now() > self.due_date
class NotificationService: def send_overdue_notification(self, book: Book, member: Member): if book.is_overdue(): email_service.send(member.email, "Book is overdue")class Book { public boolean isOverdue() { return LocalDateTime.now().isAfter(this.dueDate); }}
class NotificationService { public void sendOverdueNotification(Book book, Member member) { if (book.isOverdue()) { emailService.send(member.getEmail(), "Book is overdue"); } }}Part 5: Responsibility Assignment Framework
Section titled “Part 5: Responsibility Assignment Framework”Decision Framework
Section titled “Decision Framework”graph TD
A[What needs to be done?] --> B{Is it data management?}
B -->|Yes| C[Data Holder/Repository]
B -->|No| D{Is it business logic?}
D -->|Yes| E[Business Logic Class]
D -->|No| F{Is it coordination?}
F -->|Yes| G[Service/Coordinator]
F -->|No| H{Is it validation?}
H -->|Yes| I[Validator]
H -->|No| J{Is it communication?}
J -->|Yes| K[Service/Adapter]
style C fill:#dbeafe
style E fill:#d1fae5
style G fill:#fef3c7
style I fill:#fee2e2
style K fill:#e5e7eb
Questions to Ask
Section titled “Questions to Ask”For each responsibility, ask:
- Who owns this data? → Assign to that class
- What is the primary purpose? → Assign to class with that purpose
- Does it need coordination? → Create a coordinator/service
- Is it reusable? → Create a separate utility/service
- Does it violate SRP? → Split into multiple classes
Entities:
- ParkingLot
- ParkingSpot
- Vehicle
- Ticket
- Payment
Responsibility Assignment:
ParkingLot Responsibilities
Section titled “ParkingLot Responsibilities”What should ParkingLot do?
- Manage collection of parking spots
- Find available spots
- Assign spots to vehicles
- Release spots when vehicles leave
ParkingLot Class:
class ParkingLot: def __init__(self, capacity: int): self.spots: List[ParkingSpot] = [] self.capacity = capacity
# Coordination responsibility def park_vehicle(self, vehicle: Vehicle) -> Ticket: spot = self._find_available_spot(vehicle.get_type()) if not spot: raise ValueError("No available spots")
spot.park_vehicle(vehicle) return Ticket(vehicle, spot, datetime.now())
def unpark_vehicle(self, ticket: Ticket) -> Payment: spot = ticket.spot vehicle = spot.unpark_vehicle() duration = ticket.calculate_duration() amount = self._calculate_payment(duration, vehicle.get_type()) return Payment(ticket, amount)
# Helper methods def _find_available_spot(self, vehicle_type: VehicleType) -> Optional[ParkingSpot]: for spot in self.spots: if spot.is_available() and spot.get_type() == vehicle_type: return spot return None
def _calculate_payment(self, duration: float, vehicle_type: VehicleType) -> float: base_rate = 10.0 # $10 per hour if vehicle_type == VehicleType.TRUCK: base_rate *= 1.5 # Trucks pay 50% more return duration * base_rateclass ParkingLot { private List<ParkingSpot> spots; private int capacity;
public ParkingLot(int capacity) { this.spots = new ArrayList<>(); this.capacity = capacity; }
// Coordination responsibility public Ticket parkVehicle(Vehicle vehicle) { ParkingSpot spot = findAvailableSpot(vehicle.getType()); if (spot == null) { throw new IllegalStateException("No available spots"); }
spot.parkVehicle(vehicle); return new Ticket(vehicle, spot, LocalDateTime.now()); }
public Payment unparkVehicle(Ticket ticket) { ParkingSpot spot = ticket.getSpot(); Vehicle vehicle = spot.unparkVehicle(); double duration = ticket.calculateDuration(); double amount = calculatePayment(duration, vehicle.getType()); return new Payment(ticket, amount); }
// Helper methods private ParkingSpot findAvailableSpot(VehicleType vehicleType) { for (ParkingSpot spot : spots) { if (spot.isAvailable() && spot.getType() == vehicleType) { return spot; } } return null; }
private double calculatePayment(double duration, VehicleType vehicleType) { double baseRate = 10.0; // $10 per hour if (vehicleType == VehicleType.TRUCK) { baseRate *= 1.5; // Trucks pay 50% more } return duration * baseRate; }}Responsibility: Coordinate parking operations
ParkingSpot Responsibilities
Section titled “ParkingSpot Responsibilities”What should ParkingSpot do?
- Track its own state (occupied/available)
- Know its type and location
- Park/unpark vehicles
ParkingSpot Class:
class ParkingSpot: def __init__(self, spot_id: str, spot_type: VehicleType): self.spot_id = spot_id self.spot_type = spot_type self._is_occupied = False self._vehicle: Optional[Vehicle] = None
# State management responsibility def park_vehicle(self, vehicle: Vehicle) -> None: if not self.is_available(): raise ValueError("Spot is already occupied") if vehicle.get_type() != self.spot_type: raise ValueError("Vehicle type doesn't match spot type")
self._vehicle = vehicle self._is_occupied = True
def unpark_vehicle(self) -> Vehicle: if not self._is_occupied: raise ValueError("Spot is not occupied")
vehicle = self._vehicle self._vehicle = None self._is_occupied = False return vehicle
def is_available(self) -> bool: return not self._is_occupied
def get_type(self) -> VehicleType: return self.spot_typeclass ParkingSpot { private String spotId; private VehicleType spotType; private boolean isOccupied; private Vehicle vehicle;
public ParkingSpot(String spotId, VehicleType spotType) { this.spotId = spotId; this.spotType = spotType; this.isOccupied = false; this.vehicle = null; }
// State management responsibility public void parkVehicle(Vehicle vehicle) { if (!isAvailable()) { throw new IllegalStateException("Spot is already occupied"); } if (vehicle.getType() != this.spotType) { throw new IllegalArgumentException("Vehicle type doesn't match spot type"); }
this.vehicle = vehicle; this.isOccupied = true; }
public Vehicle unparkVehicle() { if (!this.isOccupied) { throw new IllegalStateException("Spot is not occupied"); }
Vehicle vehicle = this.vehicle; this.vehicle = null; this.isOccupied = false; return vehicle; }
public boolean isAvailable() { return !this.isOccupied; }
public VehicleType getType() { return this.spotType; }}Responsibility: Manage spot state and vehicle assignment
Ticket Responsibilities
Section titled “Ticket Responsibilities”What should Ticket do?
- Store entry information
- Calculate parking duration
- Reference vehicle and spot
Ticket Class:
class Ticket: def __init__(self, vehicle: Vehicle, spot: ParkingSpot, entry_time: datetime): self.ticket_id = self._generate_id() self.vehicle = vehicle self.spot = spot self.entry_time = entry_time
# Business logic responsibility def calculate_duration(self) -> float: return (datetime.now() - self.entry_time).total_seconds() / 3600
def _generate_id(self) -> str: return f"TICKET_{uuid.uuid4().hex[:8]}"class Ticket { private String ticketId; private Vehicle vehicle; private ParkingSpot spot; private LocalDateTime entryTime;
public Ticket(Vehicle vehicle, ParkingSpot spot, LocalDateTime entryTime) { this.ticketId = generateId(); this.vehicle = vehicle; this.spot = spot; this.entryTime = entryTime; }
// Business logic responsibility public double calculateDuration() { long minutes = ChronoUnit.MINUTES.between(this.entryTime, LocalDateTime.now()); return minutes / 60.0; }
private String generateId() { return "TICKET_" + UUID.randomUUID().toString().substring(0, 8); }}Responsibility: Track parking session and calculate duration
Visual: Complete Responsibility Assignment
Section titled “Visual: Complete Responsibility Assignment”classDiagram
direction LR
class ParkingLot {
+ManageSpots()
+CoordinateParking()
+CalculatePayment()
}
class ParkingSpot {
+TrackState()
+ParkUnparkVehicles()
+ValidateVehicleType()
}
class Ticket {
+StoreEntryInfo()
+CalculateDuration()
+ReferenceVehicleSpot()
}
class Payment {
+StorePaymentInfo()
+ProcessPayment()
+ReferenceTicket()
}
Part 6: Cohesion and Coupling
Section titled “Part 6: Cohesion and Coupling”Cohesion
Section titled “Cohesion”Cohesion measures how related the responsibilities within a class are.
Types:
- High Cohesion - All methods work together for one purpose
- Low Cohesion - Methods are unrelated
Visual: Cohesion
Section titled “Visual: Cohesion”Coupling
Section titled “Coupling”Coupling measures how dependent classes are on each other.
Types:
Visual: Coupling
Section titled “Visual: Coupling”Best Practices
Section titled “Best Practices”Aim for:
- High Cohesion - Related responsibilities together
- Low Coupling - Independent classes
- Single Responsibility - One reason to change
Summary: Assign Responsibilities
Section titled “Summary: Assign Responsibilities”Key Takeaways
Section titled “Key Takeaways”- Single Responsibility Principle - One reason to change
- Identify what each class needs to do - Group related operations
- Avoid God Classes - Split large classes
- Avoid Anemic Models - Include behavior with data
- High Cohesion - Related responsibilities together
- Low Coupling - Independent classes
- Use patterns - Data Holder, Business Logic, Coordinator, Validator, Repository
Responsibility Checklist
Section titled “Responsibility Checklist”When assigning responsibilities, ensure:
- Each class has one primary responsibility
- Responsibilities are related (high cohesion)
- Classes are independent (low coupling)
- No God Classes - Split if too many responsibilities
- No Anemic Models - Include behavior with data
- Clear purpose - Easy to understand what class does
Visual Summary
Section titled “Visual Summary”Next Steps
Section titled “Next Steps”Now that you’ve mastered assigning responsibilities, let’s learn how to create class diagrams:
This next guide will teach you how to visualize your design with class diagrams, showing relationships, inheritance, and composition!