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; }}class Book { private isbn: string; private title: string; private author: string; private available: boolean;
// Data Management constructor(isbn: string, title: string, author: string) { this.isbn = isbn; this.title = title; this.author = author; this.available = true; }
// State Management markAsBorrowed(): void { this.available = false; }
markAsAvailable(): void { this.available = true; }
isAvailable(): boolean { return this.available; }}class Book {private: std::string isbn; std::string title; std::string author; bool available;
public: // Data Management Book(const std::string& isbn, const std::string& title, const std::string& author) : isbn(isbn), title(title), author(author), available(true) {}
// State Management void markAsBorrowed() { available = false; }
void markAsAvailable() { available = true; }
bool isAvailable() const { return available; }};class Book { private string isbn; private string title; private string author; private bool 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 bool 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(); }}class Loan { private book: Book; private member: Member; private loanDate: Date; private dueDate: Date; private returnDate: Date | null;
constructor(book: Book, member: Member, loanDate: Date) { this.book = book; this.member = member; this.loanDate = loanDate; this.dueDate = this.calculateDueDate(); this.returnDate = null; }
// Business Logic private calculateDueDate(): Date { const due = new Date(this.loanDate); due.setDate(due.getDate() + 14); return due; }
isOverdue(): boolean { if (this.returnDate) { return false; } return new Date() > this.dueDate; }
calculateFine(): number { if (!this.isOverdue()) { return 0.0; } const daysOverdue = Math.floor( (new Date().getTime() - this.dueDate.getTime()) / (1000 * 60 * 60 * 24) ); return daysOverdue * 1.0; // $1 per day }
// State Management returnBook(): void { this.returnDate = new Date(); }}#include <chrono>#include <optional>
class Loan {private: Book book; Member member; std::chrono::system_clock::time_point loanDate; std::chrono::system_clock::time_point dueDate; std::optional<std::chrono::system_clock::time_point> returnDate;
// Business Logic std::chrono::system_clock::time_point calculateDueDate() { return loanDate + std::chrono::hours(24 * 14); }
public: Loan(const Book& book, const Member& member, const std::chrono::system_clock::time_point& loanDate) : book(book), member(member), loanDate(loanDate), dueDate(calculateDueDate()), returnDate(std::nullopt) {}
bool isOverdue() const { if (returnDate.has_value()) { return false; } return std::chrono::system_clock::now() > dueDate; }
double calculateFine() const { if (!isOverdue()) { return 0.0; } auto now = std::chrono::system_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::hours>(now - dueDate); int daysOverdue = duration.count() / 24; return daysOverdue * 1.0; // $1 per day }
// State Management void returnBook() { returnDate = std::chrono::system_clock::now(); }};using System;
class Loan { private Book book; private Member member; private DateTime loanDate; private DateTime dueDate; private DateTime? returnDate;
public Loan(Book book, Member member, DateTime loanDate) { this.book = book; this.member = member; this.loanDate = loanDate; this.dueDate = CalculateDueDate(); this.returnDate = null; }
// Business Logic private DateTime CalculateDueDate() { return this.loanDate.AddDays(14); }
public bool IsOverdue() { if (this.returnDate.HasValue) { return false; } return DateTime.Now > this.dueDate; }
public double CalculateFine() { if (!IsOverdue()) { return 0.0; } int daysOverdue = (DateTime.Now - this.dueDate).Days; return daysOverdue * 1.0; // $1 per day }
// State Management public void ReturnBook() { this.returnDate = DateTime.Now; }}Responsibility: Manage loan lifecycle and calculate fines
Visual: Responsibility Assignment
Section titled “Visual: Responsibility Assignment”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; }}class Member { private memberId: string; private name: string; private email: string; private membershipType: string;
constructor(memberId: string, name: string, email: string) { this.memberId = memberId; this.name = name; this.email = email; this.membershipType = "STANDARD"; }
updateMembership(newType: string): void { this.membershipType = newType; }}#include <string>
class Member {private: std::string memberId; std::string name; std::string email; std::string membershipType;
public: Member(const std::string& memberId, const std::string& name, const std::string& email) : memberId(memberId), name(name), email(email), membershipType("STANDARD") {}
void updateMembership(const std::string& newType) { membershipType = newType; }};class 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; } }}class FineCalculator { calculateFine(loan: Loan): number { if (!loan.isOverdue()) { return 0.0; }
const daysOverdue = Math.floor( (new Date().getTime() - loan.getDueDate().getTime()) / (1000 * 60 * 60 * 24) );
// 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; } }}#include <chrono>
class FineCalculator {public: double calculateFine(const Loan& loan) { if (!loan.isOverdue()) { return 0.0; }
auto now = std::chrono::system_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::hours>( now - loan.getDueDate() ); int daysOverdue = duration.count() / 24;
// 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; } }};using System;
class FineCalculator { public double CalculateFine(Loan loan) { if (!loan.IsOverdue()) { return 0.0; }
int daysOverdue = (DateTime.Now - loan.GetDueDate()).Days;
// 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; }}class LibraryService { private bookRepository: BookRepository; private loanRepository: LoanRepository; private fineCalculator: FineCalculator;
constructor(bookRepository: BookRepository, loanRepository: LoanRepository, fineCalculator: FineCalculator) { this.bookRepository = bookRepository; this.loanRepository = loanRepository; this.fineCalculator = fineCalculator; }
borrowBook(bookId: string, memberId: string): Loan { // Coordinate between multiple classes const book = this.bookRepository.findById(bookId); const member = this.memberRepository.findById(memberId);
if (!book.isAvailable()) { throw new Error("Book not available"); }
const loan = new Loan(book, member, new Date()); this.loanRepository.save(loan); book.markAsBorrowed();
return loan; }}#include <memory>#include <stdexcept>
class LibraryService {private: std::shared_ptr<BookRepository> bookRepository; std::shared_ptr<LoanRepository> loanRepository; std::shared_ptr<FineCalculator> fineCalculator;
public: LibraryService(std::shared_ptr<BookRepository> bookRepo, std::shared_ptr<LoanRepository> loanRepo, std::shared_ptr<FineCalculator> fineCalc) : bookRepository(bookRepo), loanRepository(loanRepo), fineCalculator(fineCalc) {}
Loan borrowBook(const std::string& bookId, const std::string& memberId) { // Coordinate between multiple classes Book book = bookRepository->findById(bookId); Member member = memberRepository->findById(memberId);
if (!book.isAvailable()) { throw std::runtime_error("Book not available"); }
Loan loan(book, member, std::chrono::system_clock::now()); loanRepository->save(loan); book.markAsBorrowed();
return loan; }};using System;
class 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 InvalidOperationException("Book not available"); }
Loan loan = new Loan(book, member, DateTime.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; }}class LoanValidator { validateLoan(book: Book, member: Member): boolean { if (!book.isAvailable()) { throw new Error("Book is not available"); }
if (member.hasOverdueBooks()) { throw new Error("Member has overdue books"); }
if (member.getActiveLoansCount() >= 5) { throw new Error("Member has reached loan limit"); }
return true; }}#include <stdexcept>
class LoanValidator {public: bool validateLoan(const Book& book, const Member& member) { if (!book.isAvailable()) { throw std::runtime_error("Book is not available"); }
if (member.hasOverdueBooks()) { throw std::runtime_error("Member has overdue books"); }
if (member.getActiveLoansCount() >= 5) { throw std::runtime_error("Member has reached loan limit"); }
return true; }};using System;
class LoanValidator { public bool ValidateLoan(Book book, Member member) { if (!book.IsAvailable()) { throw new InvalidOperationException("Book is not available"); }
if (member.HasOverdueBooks()) { throw new InvalidOperationException("Member has overdue books"); }
if (member.GetActiveLoansCount() >= 5) { throw new InvalidOperationException("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<>(); }}class BookRepository { save(book: Book): void { // Save to database }
findById(bookId: string): Book { // Retrieve from database return null; }
findAvailableBooks(): Book[] { // Query database return []; }}#include <vector>#include <optional>
class BookRepository {public: void save(const Book& book) { // Save to database }
std::optional<Book> findById(const std::string& bookId) { // Retrieve from database return std::nullopt; }
std::vector<Book> findAvailableBooks() { // Query database return std::vector<Book>(); }};using System.Collections.Generic;
class 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 List<Book>(); }}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) {}}class LibraryManager { // Too many responsibilities! addBook(book: Book): void {} removeBook(bookId: string): void {} borrowBook(bookId: string, memberId: string): void {} returnBook(loanId: string): void {} calculateFine(loanId: string): number { return 0.0; } sendNotification(memberId: string, message: string): void {} generateReport(): Report { return null; } processPayment(memberId: string, amount: number): void {}}class LibraryManager {public: // Too many responsibilities! void addBook(const Book& book) {} void removeBook(const std::string& bookId) {} void borrowBook(const std::string& bookId, const std::string& memberId) {} void returnBook(const std::string& loanId) {} double calculateFine(const std::string& loanId) { return 0.0; } void sendNotification(const std::string& memberId, const std::string& message) {} Report generateReport() { return Report(); } void processPayment(const std::string& memberId, double amount) {}};class 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) {}}class BookService { addBook(book: Book): void {} removeBook(bookId: string): void {}}
class LoanService { borrowBook(bookId: string, memberId: string): void {} returnBook(loanId: string): void {}}
class FineService { calculateFine(loanId: string): number { return 0.0; }}
class NotificationService { sendNotification(memberId: string, message: string): void {}}
class ReportService { generateReport(): Report { return null; }}
class PaymentService { processPayment(memberId: string, amount: number): void {}}class BookService {public: void addBook(const Book& book) {} void removeBook(const std::string& bookId) {}};
class LoanService {public: void borrowBook(const std::string& bookId, const std::string& memberId) {} void returnBook(const std::string& loanId) {}};
class FineService {public: double calculateFine(const std::string& loanId) { return 0.0; }};
class NotificationService {public: void sendNotification(const std::string& memberId, const std::string& message) {}};
class ReportService {public: Report generateReport() { return Report(); }};
class PaymentService {public: void processPayment(const std::string& memberId, double amount) {}};class 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; }}class Book { // Only data, no behavior! isbn: string; title: string; isAvailable: boolean;
constructor(isbn: string, title: string) { this.isbn = isbn; this.title = title; this.isAvailable = true; }}
class BookManager { // All logic in manager markAsBorrowed(book: Book): void { book.isAvailable = false; }
markAsAvailable(book: Book): void { book.isAvailable = true; }}class Book {public: // Only data, no behavior! std::string isbn; std::string title; bool isAvailable;
Book(const std::string& isbn, const std::string& title) : isbn(isbn), title(title), isAvailable(true) {}};
class BookManager { // All logic in managerpublic: void markAsBorrowed(Book& book) { book.isAvailable = false; }
void markAsAvailable(Book& book) { book.isAvailable = true; }};class Book { // Only data, no behavior! public string isbn; public string title; public bool 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; }}class Book { private isbn: string; private title: string; private _isAvailable: boolean;
constructor(isbn: string, title: string) { this.isbn = isbn; this.title = title; this._isAvailable = true; }
// Behavior with data markAsBorrowed(): void { this._isAvailable = false; }
markAsAvailable(): void { this._isAvailable = true; }
isAvailable(): boolean { return this._isAvailable; }}class Book {private: std::string isbn; std::string title; bool _isAvailable;
public: Book(const std::string& isbn, const std::string& title) : isbn(isbn), title(title), _isAvailable(true) {}
// Behavior with data void markAsBorrowed() { _isAvailable = false; }
void markAsAvailable() { _isAvailable = true; }
bool isAvailable() const { return _isAvailable; }};class Book { private string isbn; private string title; private bool 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 bool 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"); }}class Book { sendOverdueNotification(member: Member): void { // Book shouldn't send notifications! emailService.send(member.email, "Book is overdue"); }}class Book {public: void sendOverdueNotification(const Member& member) { // Book shouldn't send notifications! emailService.send(member.getEmail(), "Book is overdue"); }};class Book { public void SendOverdueNotification(Member member) { // Book shouldn't send notifications! emailService.Send(member.Email, "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"); } }}class Book { isOverdue(): boolean { return new Date() > this.dueDate; }}
class NotificationService { sendOverdueNotification(book: Book, member: Member): void { if (book.isOverdue()) { emailService.send(member.email, "Book is overdue"); } }}class Book {public: bool isOverdue() const { return std::chrono::system_clock::now() > dueDate; }};
class NotificationService {public: void sendOverdueNotification(const Book& book, const Member& member) { if (book.isOverdue()) { emailService.send(member.getEmail(), "Book is overdue"); } }};class Book { public bool IsOverdue() { return DateTime.Now > this.dueDate; }}
class NotificationService { public void SendOverdueNotification(Book book, Member member) { if (book.IsOverdue()) { emailService.Send(member.Email, "Book is overdue"); } }}Part 5: Responsibility Assignment Framework
Section titled “Part 5: Responsibility Assignment Framework”Decision Framework
Section titled “Decision Framework”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; }}class ParkingLot { private spots: ParkingSpot[]; private capacity: number;
constructor(capacity: number) { this.spots = []; this.capacity = capacity; }
// Coordination responsibility parkVehicle(vehicle: Vehicle): Ticket { const spot = this.findAvailableSpot(vehicle.getType()); if (!spot) { throw new Error("No available spots"); }
spot.parkVehicle(vehicle); return new Ticket(vehicle, spot, new Date()); }
unparkVehicle(ticket: Ticket): Payment { const spot = ticket.getSpot(); const vehicle = spot.unparkVehicle(); const duration = ticket.calculateDuration(); const amount = this.calculatePayment(duration, vehicle.getType()); return new Payment(ticket, amount); }
// Helper methods private findAvailableSpot(vehicleType: VehicleType): ParkingSpot | null { for (const spot of this.spots) { if (spot.isAvailable() && spot.getType() === vehicleType) { return spot; } } return null; }
private calculatePayment(duration: number, vehicleType: VehicleType): number { let baseRate = 10.0; // $10 per hour if (vehicleType === VehicleType.TRUCK) { baseRate *= 1.5; // Trucks pay 50% more } return duration * baseRate; }}#include <vector>#include <optional>#include <stdexcept>
class ParkingLot {private: std::vector<ParkingSpot> spots; int capacity;
// Helper methods std::optional<ParkingSpot*> findAvailableSpot(VehicleType vehicleType) { for (auto& spot : spots) { if (spot.isAvailable() && spot.getType() == vehicleType) { return &spot; } } return std::nullopt; }
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; }
public: ParkingLot(int capacity) : capacity(capacity) {}
// Coordination responsibility Ticket parkVehicle(const Vehicle& vehicle) { auto spotOpt = findAvailableSpot(vehicle.getType()); if (!spotOpt.has_value()) { throw std::runtime_error("No available spots"); }
ParkingSpot* spot = spotOpt.value(); spot->parkVehicle(vehicle); return Ticket(vehicle, *spot, std::chrono::system_clock::now()); }
Payment unparkVehicle(const Ticket& ticket) { ParkingSpot& spot = ticket.getSpot(); Vehicle vehicle = spot.unparkVehicle(); double duration = ticket.calculateDuration(); double amount = calculatePayment(duration, vehicle.getType()); return Payment(ticket, amount); }};using System;using System.Collections.Generic;using System.Linq;
class ParkingLot { private List<ParkingSpot> spots; private int capacity;
public ParkingLot(int capacity) { this.spots = new List<ParkingSpot>(); this.capacity = capacity; }
// Coordination responsibility public Ticket ParkVehicle(Vehicle vehicle) { ParkingSpot spot = FindAvailableSpot(vehicle.GetType()); if (spot == null) { throw new InvalidOperationException("No available spots"); }
spot.ParkVehicle(vehicle); return new Ticket(vehicle, spot, DateTime.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) { return spots.FirstOrDefault(spot => spot.IsAvailable() && spot.GetType() == vehicleType); }
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; }}class ParkingSpot { private spotId: string; private spotType: VehicleType; private _isOccupied: boolean; private _vehicle: Vehicle | null;
constructor(spotId: string, spotType: VehicleType) { this.spotId = spotId; this.spotType = spotType; this._isOccupied = false; this._vehicle = null; }
// State management responsibility parkVehicle(vehicle: Vehicle): void { if (!this.isAvailable()) { throw new Error("Spot is already occupied"); } if (vehicle.getType() !== this.spotType) { throw new Error("Vehicle type doesn't match spot type"); }
this._vehicle = vehicle; this._isOccupied = true; }
unparkVehicle(): Vehicle { if (!this._isOccupied) { throw new Error("Spot is not occupied"); }
const vehicle = this._vehicle!; this._vehicle = null; this._isOccupied = false; return vehicle; }
isAvailable(): boolean { return !this._isOccupied; }
getType(): VehicleType { return this.spotType; }}#include <string>#include <optional>#include <stdexcept>
class ParkingSpot {private: std::string spotId; VehicleType spotType; bool isOccupied; std::optional<Vehicle> vehicle;
public: ParkingSpot(const std::string& spotId, VehicleType spotType) : spotId(spotId), spotType(spotType), isOccupied(false), vehicle(std::nullopt) {}
// State management responsibility void parkVehicle(const Vehicle& v) { if (!isAvailable()) { throw std::runtime_error("Spot is already occupied"); } if (v.getType() != spotType) { throw std::invalid_argument("Vehicle type doesn't match spot type"); }
vehicle = v; isOccupied = true; }
Vehicle unparkVehicle() { if (!isOccupied) { throw std::runtime_error("Spot is not occupied"); }
Vehicle v = vehicle.value(); vehicle = std::nullopt; isOccupied = false; return v; }
bool isAvailable() const { return !isOccupied; }
VehicleType getType() const { return spotType; }};using System;
class ParkingSpot { private string spotId; private VehicleType spotType; private bool 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 InvalidOperationException("Spot is already occupied"); } if (vehicle.GetType() != this.spotType) { throw new ArgumentException("Vehicle type doesn't match spot type"); }
this.vehicle = vehicle; this.isOccupied = true; }
public Vehicle UnparkVehicle() { if (!this.isOccupied) { throw new InvalidOperationException("Spot is not occupied"); }
Vehicle vehicle = this.vehicle; this.vehicle = null; this.isOccupied = false; return vehicle; }
public bool 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); }}class Ticket { private ticketId: string; private vehicle: Vehicle; private spot: ParkingSpot; private entryTime: Date;
constructor(vehicle: Vehicle, spot: ParkingSpot, entryTime: Date) { this.ticketId = this.generateId(); this.vehicle = vehicle; this.spot = spot; this.entryTime = entryTime; }
// Business logic responsibility calculateDuration(): number { const now = new Date(); const durationMs = now.getTime() - this.entryTime.getTime(); return durationMs / (1000 * 60 * 60); // Convert to hours }
private generateId(): string { return `TICKET_${Math.random().toString(36).substring(2, 10)}`; }}#include <chrono>#include <string>#include <random>
class Ticket {private: std::string ticketId; Vehicle vehicle; ParkingSpot spot; std::chrono::system_clock::time_point entryTime;
std::string generateId() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, 15);
std::string id = "TICKET_"; for (int i = 0; i < 8; i++) { id += "0123456789abcdef"[dis(gen)]; } return id; }
public: Ticket(const Vehicle& vehicle, const ParkingSpot& spot, const std::chrono::system_clock::time_point& entryTime) : ticketId(generateId()), vehicle(vehicle), spot(spot), entryTime(entryTime) {}
// Business logic responsibility double calculateDuration() const { auto now = std::chrono::system_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::hours>(now - entryTime); return duration.count(); }};using System;
class Ticket { private string ticketId; private Vehicle vehicle; private ParkingSpot spot; private DateTime entryTime;
public Ticket(Vehicle vehicle, ParkingSpot spot, DateTime entryTime) { this.ticketId = GenerateId(); this.vehicle = vehicle; this.spot = spot; this.entryTime = entryTime; }
// Business logic responsibility public double CalculateDuration() { TimeSpan duration = DateTime.Now - this.entryTime; return duration.TotalHours; }
private string GenerateId() { return $"TICKET_{Guid.NewGuid().ToString().Substring(0, 8)}"; }}Responsibility: Track parking session and calculate duration
Visual: Complete Responsibility Assignment
Section titled “Visual: Complete Responsibility Assignment”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!