Loading content...
Design patterns aren't theoretical exercises—they're proven solutions to recurring problems. Our Library Management System presents several opportunities to apply patterns that add flexibility, maintainability, and clarity to the design.
Patterns We'll Apply:
| Pattern | Problem Solved | Where Applied |
|---|---|---|
| Observer | Notifying members when reserved books become available | Reservation→Member notification |
| State | Managing complex status transitions for loans and copies | LoanStatus, BookCopyStatus |
| Strategy | Different rules for different membership types | Fine calculation, borrowing limits |
Each pattern addresses a specific design challenge. We'll refactor our existing code to incorporate these patterns, showing the before-and-after transformation.
By the end of this page, you will be able to: • Apply the Observer pattern for event-driven notifications • Implement the State pattern for complex status management • Use the Strategy pattern for pluggable business rules • Recognize when patterns add value vs. unnecessary complexity • Refactor existing code to incorporate patterns cleanly
The Problem:
When a reserved book is returned, the next member in the reservation queue must be notified. Additionally, we might want to:
Without Observer:
The return logic would directly call notification services, creating tight coupling:
// ❌ Tightly coupled - hard to extend
function returnBook(copy: BookCopy) {
copy.checkIn();
const reservation = book.getNextPendingReservation();
if (reservation) {
emailService.sendReservationReady(reservation);
smsService.sendReservationReady(reservation);
analyticsService.trackReservationFulfilled(reservation);
// Adding another notification type requires modifying this code
}
}
With Observer:
The return logic publishes an event; interested parties subscribe to react:
// ✅ Loosely coupled - easy to extend
function returnBook(copy: BookCopy) {
copy.checkIn();
const reservation = book.getNextPendingReservation();
if (reservation) {
eventBus.publish(new ReservationReadyEvent(reservation));
// New notification channels just subscribe - no code changes here
}
}
Observer Pattern Structure:
The pattern involves three components:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
// ============================================// Domain Events// ============================================ /** * Base interface for all library domain events */interface LibraryEvent { readonly eventType: string; readonly timestamp: Date; readonly eventId: string;} /** * Published when a reserved book becomes available */class ReservationReadyEvent implements LibraryEvent { readonly eventType = 'RESERVATION_READY'; readonly timestamp: Date; readonly eventId: string; readonly reservation: Reservation; readonly book: Book; readonly member: Member; constructor(reservation: Reservation) { this.timestamp = new Date(); this.eventId = crypto.randomUUID(); this.reservation = reservation; this.book = reservation.getBook(); this.member = reservation.getMember(); }} /** * Published when a loan becomes overdue */class LoanOverdueEvent implements LibraryEvent { readonly eventType = 'LOAN_OVERDUE'; readonly timestamp: Date; readonly eventId: string; readonly loan: Loan; readonly member: Member; readonly daysOverdue: number; constructor(loan: Loan) { this.timestamp = new Date(); this.eventId = crypto.randomUUID(); this.loan = loan; this.member = loan.getMember(); this.daysOverdue = loan.getOverdueDays(); }} /** * Published when a book is returned */class BookReturnedEvent implements LibraryEvent { readonly eventType = 'BOOK_RETURNED'; readonly timestamp: Date; readonly eventId: string; readonly loan: Loan; readonly wasOverdue: boolean; readonly fineAmount: number; constructor(loan: Loan, fineResult: FineResult) { this.timestamp = new Date(); this.eventId = crypto.randomUUID(); this.loan = loan; this.wasOverdue = fineResult.wasOverdue; this.fineAmount = fineResult.fineAmount; }} // ============================================// Observer Interface// ============================================ /** * Generic observer interface for library events */interface LibraryEventObserver { /** * Called when a relevant event occurs */ onEvent(event: LibraryEvent): void; /** * Returns the event types this observer is interested in */ getSubscribedEventTypes(): string[];} // ============================================// Event Bus (Subject)// ============================================ /** * Central event bus for publishing and subscribing to library events */class LibraryEventBus { private static instance: LibraryEventBus; private observers: Map<string, Set<LibraryEventObserver>> = new Map(); private constructor() {} static getInstance(): LibraryEventBus { if (!LibraryEventBus.instance) { LibraryEventBus.instance = new LibraryEventBus(); } return LibraryEventBus.instance; } /** * Subscribe an observer to receive events */ subscribe(observer: LibraryEventObserver): void { const eventTypes = observer.getSubscribedEventTypes(); for (const eventType of eventTypes) { if (!this.observers.has(eventType)) { this.observers.set(eventType, new Set()); } this.observers.get(eventType)!.add(observer); } } /** * Unsubscribe an observer from all events */ unsubscribe(observer: LibraryEventObserver): void { for (const observers of this.observers.values()) { observers.delete(observer); } } /** * Publish an event to all interested observers */ publish(event: LibraryEvent): void { const eventObservers = this.observers.get(event.eventType); if (eventObservers) { for (const observer of eventObservers) { try { observer.onEvent(event); } catch (error) { // Log but don't stop other observers console.error( `Observer error for ${event.eventType}:`, error ); } } } }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
// ============================================// Concrete Observers// ============================================ /** * Sends email notifications for library events */class EmailNotificationObserver implements LibraryEventObserver { private readonly emailService: EmailService; constructor(emailService: EmailService) { this.emailService = emailService; } getSubscribedEventTypes(): string[] { return ['RESERVATION_READY', 'LOAN_OVERDUE']; } onEvent(event: LibraryEvent): void { if (event instanceof ReservationReadyEvent) { this.sendReservationReadyEmail(event); } else if (event instanceof LoanOverdueEvent) { this.sendOverdueReminderEmail(event); } } private sendReservationReadyEmail(event: ReservationReadyEvent): void { const member = event.member; const book = event.book; this.emailService.send({ to: member.getEmail(), subject: `Your reserved book is ready: ${book.getTitle()}`, body: `Dear ${member.getName()}, ` + `The book "${book.getTitle()}" you reserved is now ` + `available for pickup. Please collect it within 7 days. ` + `Location: ${book.findAvailableCopy()?.getRackLocation()}` }); } private sendOverdueReminderEmail(event: LoanOverdueEvent): void { const member = event.member; const loan = event.loan; const book = loan.getBookCopy().getBook(); this.emailService.send({ to: member.getEmail(), subject: `Overdue Notice: ${book.getTitle()}`, body: `Dear ${member.getName()}, ` + `The book "${book.getTitle()}" is ${event.daysOverdue} ` + `days overdue. Current fine: $${loan.calculateCurrentFine()} ` + `Please return it as soon as possible.` }); }} /** * Logs all events for auditing */class AuditLogObserver implements LibraryEventObserver { private readonly auditLogger: AuditLogger; constructor(auditLogger: AuditLogger) { this.auditLogger = auditLogger; } getSubscribedEventTypes(): string[] { // Subscribe to ALL events for comprehensive audit trail return [ 'RESERVATION_READY', 'LOAN_OVERDUE', 'BOOK_RETURNED', 'BOOK_BORROWED', 'FINE_PAID', 'MEMBER_SUSPENDED' ]; } onEvent(event: LibraryEvent): void { this.auditLogger.log({ eventId: event.eventId, eventType: event.eventType, timestamp: event.timestamp, details: this.extractDetails(event) }); } private extractDetails(event: LibraryEvent): Record<string, unknown> { // Extract relevant details for each event type if (event instanceof BookReturnedEvent) { return { loanId: event.loan.getId(), memberId: event.loan.getMember().getId(), wasOverdue: event.wasOverdue, fineAmount: event.fineAmount }; } // ... handle other event types return {}; }} /** * Updates analytics metrics */class AnalyticsObserver implements LibraryEventObserver { private readonly analytics: AnalyticsService; constructor(analytics: AnalyticsService) { this.analytics = analytics; } getSubscribedEventTypes(): string[] { return ['BOOK_RETURNED', 'BOOK_BORROWED', 'RESERVATION_READY']; } onEvent(event: LibraryEvent): void { this.analytics.trackEvent({ category: 'library', action: event.eventType, timestamp: event.timestamp }); }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
/** * Library service now publishes events instead of calling services directly */class LibraryService { private readonly eventBus: LibraryEventBus; private readonly bookRepository: BookRepository; private readonly loanRepository: LoanRepository; constructor( eventBus: LibraryEventBus, bookRepository: BookRepository, loanRepository: LoanRepository ) { this.eventBus = eventBus; this.bookRepository = bookRepository; this.loanRepository = loanRepository; } /** * Returns a book and triggers appropriate notifications */ async returnBook(copyBarcode: string): Promise<ReturnResult> { // Find the book and loan const book = await this.bookRepository.findByBarcode(copyBarcode); if (!book) throw new BookNotFoundError(); const copy = book.getCopyByBarcode(copyBarcode); if (!copy) throw new CopyNotFoundError(); const loan = copy.getCurrentLoan(); if (!loan) throw new NoActiveLoanError(); // Process the return const fineResult = loan.returnBook(); // Publish return event this.eventBus.publish(new BookReturnedEvent(loan, fineResult)); // Process reservation queue const nextReservation = book.processReservationQueue(copy); if (nextReservation) { // Publish reservation ready event - observers handle notification this.eventBus.publish(new ReservationReadyEvent(nextReservation)); } await this.bookRepository.save(book); await this.loanRepository.save(loan); return { success: true, fineResult, reservationNotified: nextReservation !== null }; }} // Application bootstrap: wire up observersfunction bootstrapApplication() { const eventBus = LibraryEventBus.getInstance(); // Register observers eventBus.subscribe(new EmailNotificationObserver(emailService)); eventBus.subscribe(new AuditLogObserver(auditLogger)); eventBus.subscribe(new AnalyticsObserver(analyticsService)); // Adding SMS notifications is just one line: eventBus.subscribe(new SmsNotificationObserver(smsService)); // No changes to LibraryService needed!}Open/Closed Principle: The LibraryService is closed for modification but open for extension. Adding new notification channels requires no changes to existing code.
Single Responsibility: Each observer handles one concern (email, logging, analytics).
Testability: Observers can be tested in isolation; LibraryService can be tested with mock event bus.
The Problem:
A Loan transitions through multiple states: ACTIVE → OVERDUE → RETURNED/LOST. Different operations are valid in different states, and state-dependent behavior is scattered throughout the code:
// ❌ State logic scattered and error-prone
class Loan {
renew(): void {
if (this.status === LoanStatus.RETURNED) {
throw new Error('Cannot renew returned loan');
}
if (this.status === LoanStatus.LOST) {
throw new Error('Cannot renew lost loan');
}
// ... renewal logic
}
returnBook(): void {
if (this.status === LoanStatus.RETURNED) {
throw new Error('Already returned');
}
if (this.status === LoanStatus.LOST) {
throw new Error('Book is lost, cannot return normally');
}
// ... return logic
}
}
With State Pattern:
Each state becomes a class that encapsulates state-specific behavior. The loan delegates to its current state.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
// ============================================// State Interface// ============================================ /** * Interface defining all possible loan operations. * Each state implements these methods according to its rules. */interface LoanState { /** * Returns the name of this state for display/logging */ getStateName(): string; /** * Attempts to renew the loan * @throws if renewal not allowed in current state */ renew(loan: Loan, hasReservation: boolean): void; /** * Processes book return * @returns fine information if applicable */ returnBook(loan: Loan): FineResult; /** * Reports the book as lost */ reportLost(loan: Loan): void; /** * Checks if the loan is active (book still with member) */ isActive(): boolean; /** * Gets days until due (negative if overdue) */ getDaysRemaining(loan: Loan): number;} // ============================================// Concrete States// ============================================ /** * Active state: Book is with member, within due date */class ActiveLoanState implements LoanState { private static instance: ActiveLoanState; private constructor() {} static getInstance(): ActiveLoanState { if (!ActiveLoanState.instance) { ActiveLoanState.instance = new ActiveLoanState(); } return ActiveLoanState.instance; } getStateName(): string { return 'ACTIVE'; } renew(loan: Loan, hasReservation: boolean): void { if (hasReservation) { throw new RenewalNotAllowedError( 'Cannot renew: book has pending reservation' ); } if (loan.getRenewalCount() >= Loan.MAX_RENEWALS) { throw new RenewalNotAllowedError( `Maximum renewals (${Loan.MAX_RENEWALS}) reached` ); } loan.extendDueDate(); loan.incrementRenewalCount(); // Stay in Active state } returnBook(loan: Loan): FineResult { loan.setReturnDate(new Date()); loan.transitionTo(ReturnedLoanState.getInstance()); return { wasOverdue: false, fineAmount: 0, daysOverdue: 0 }; } reportLost(loan: Loan): void { const lostFine = Loan.LOST_BOOK_FINE; loan.setFineAmount(lostFine); loan.getMember().addFine(lostFine); loan.getBookCopy().reportLost(); loan.transitionTo(LostLoanState.getInstance()); } isActive(): boolean { return true; } getDaysRemaining(loan: Loan): number { const now = new Date(); const due = loan.getDueDate(); const diffTime = due.getTime() - now.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); }} /** * Overdue state: Book is with member, past due date */class OverdueLoanState implements LoanState { private static instance: OverdueLoanState; private constructor() {} static getInstance(): OverdueLoanState { if (!OverdueLoanState.instance) { OverdueLoanState.instance = new OverdueLoanState(); } return OverdueLoanState.instance; } getStateName(): string { return 'OVERDUE'; } renew(loan: Loan, hasReservation: boolean): void { if (hasReservation) { throw new RenewalNotAllowedError( 'Cannot renew: book has pending reservation' ); } if (loan.getRenewalCount() >= Loan.MAX_RENEWALS) { throw new RenewalNotAllowedError( `Maximum renewals (${Loan.MAX_RENEWALS}) reached` ); } // Renewing from overdue: calculate current fine first const currentFine = this.calculateFine(loan); loan.getMember().addFine(currentFine); loan.accumulateFine(currentFine); // Now extend and transition back to active loan.extendDueDate(); loan.incrementRenewalCount(); loan.transitionTo(ActiveLoanState.getInstance()); } returnBook(loan: Loan): FineResult { loan.setReturnDate(new Date()); const fine = this.calculateFine(loan); loan.setFineAmount(fine); loan.getMember().addFine(fine); loan.transitionTo(ReturnedLoanState.getInstance()); return { wasOverdue: true, fineAmount: fine, daysOverdue: this.getOverdueDays(loan) }; } reportLost(loan: Loan): void { // Lost fine + accumulated overdue fine const overdueFine = this.calculateFine(loan); const totalFine = Loan.LOST_BOOK_FINE + overdueFine; loan.setFineAmount(totalFine); loan.getMember().addFine(totalFine); loan.getBookCopy().reportLost(); loan.transitionTo(LostLoanState.getInstance()); } isActive(): boolean { return true; // Still active - book is with member } getDaysRemaining(loan: Loan): number { return -this.getOverdueDays(loan); } private getOverdueDays(loan: Loan): number { const checkDate = loan.getReturnDate() || new Date(); const due = loan.getDueDate(); const diffTime = checkDate.getTime() - due.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } private calculateFine(loan: Loan): number { const days = this.getOverdueDays(loan); const rate = loan.getMember().getDailyFineRate(); const fine = days * rate; return Math.min(fine, Loan.MAX_FINE_PER_ITEM); }} /** * Returned state: Book has been returned (terminal state) */class ReturnedLoanState implements LoanState { private static instance: ReturnedLoanState; private constructor() {} static getInstance(): ReturnedLoanState { if (!ReturnedLoanState.instance) { ReturnedLoanState.instance = new ReturnedLoanState(); } return ReturnedLoanState.instance; } getStateName(): string { return 'RETURNED'; } renew(loan: Loan, hasReservation: boolean): void { throw new InvalidOperationError( 'Cannot renew: loan has been returned' ); } returnBook(loan: Loan): FineResult { throw new InvalidOperationError( 'Loan has already been returned' ); } reportLost(loan: Loan): void { throw new InvalidOperationError( 'Cannot report returned book as lost' ); } isActive(): boolean { return false; } getDaysRemaining(loan: Loan): number { return 0; // N/A for returned loans }} /** * Lost state: Book has been reported lost (terminal state) */class LostLoanState implements LoanState { private static instance: LostLoanState; private constructor() {} static getInstance(): LostLoanState { if (!LostLoanState.instance) { LostLoanState.instance = new LostLoanState(); } return LostLoanState.instance; } getStateName(): string { return 'LOST'; } renew(loan: Loan, hasReservation: boolean): void { throw new InvalidOperationError( 'Cannot renew: book is lost' ); } returnBook(loan: Loan): FineResult { throw new InvalidOperationError( 'Cannot return: book was reported lost' ); } reportLost(loan: Loan): void { throw new InvalidOperationError( 'Book is already reported as lost' ); } isActive(): boolean { return false; } getDaysRemaining(loan: Loan): number { return 0; // N/A for lost loans }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
/** * Loan class refactored to use State pattern. * Behavior is delegated to current state object. */class Loan { static readonly MAX_RENEWALS = 2; static readonly MAX_FINE_PER_ITEM = 25.00; static readonly LOST_BOOK_FINE = 50.00; private readonly id: string; private readonly member: Member; private readonly bookCopy: BookCopy; private readonly checkoutDate: Date; private dueDate: Date; private returnDate: Date | null = null; private renewalCount: number = 0; private fineAmount: number = 0; // State pattern: current state object private state: LoanState; constructor( id: string, member: Member, bookCopy: BookCopy, processedBy: Librarian ) { this.id = id; this.member = member; this.bookCopy = bookCopy; this.checkoutDate = new Date(); this.dueDate = this.calculateDueDate(); this.state = ActiveLoanState.getInstance(); } // ============ Delegated Operations ============ /** * Renew delegates to state */ renew(hasReservation: boolean): void { this.state.renew(this, hasReservation); } /** * Return delegates to state */ returnBook(): FineResult { return this.state.returnBook(this); } /** * Report lost delegates to state */ reportLost(): void { this.state.reportLost(this); } /** * Check overdue status and transition if needed * Called periodically by a scheduled job */ checkOverdueStatus(): void { if (this.state.isActive() && this.isOverdue()) { this.transitionTo(OverdueLoanState.getInstance()); } } // ============ Query Methods ============ isActive(): boolean { return this.state.isActive(); } getStatus(): string { return this.state.getStateName(); } getDaysRemaining(): number { return this.state.getDaysRemaining(this); } // ============ State Transition ============ /** * Transitions to a new state. * Internal method called by state objects. */ transitionTo(newState: LoanState): void { console.log( `Loan ${this.id}: ${this.state.getStateName()} → ${newState.getStateName()}` ); this.state = newState; } // ============ Internal Methods for State Objects ============ extendDueDate(): void { const newDue = new Date(); newDue.setDate(newDue.getDate() + this.member.getLoanPeriodDays()); this.dueDate = newDue; } incrementRenewalCount(): void { this.renewalCount++; } setReturnDate(date: Date): void { this.returnDate = date; } setFineAmount(amount: number): void { this.fineAmount = amount; } accumulateFine(amount: number): void { this.fineAmount += amount; } private isOverdue(): boolean { return new Date() > this.dueDate; } private calculateDueDate(): Date { const due = new Date(); due.setDate(due.getDate() + this.member.getLoanPeriodDays()); return due; } // ============ Getters ============ getId(): string { return this.id; } getMember(): Member { return this.member; } getBookCopy(): BookCopy { return this.bookCopy; } getDueDate(): Date { return new Date(this.dueDate); } getReturnDate(): Date | null { return this.returnDate ? new Date(this.returnDate) : null; } getRenewalCount(): number { return this.renewalCount; } getFineAmount(): number { return this.fineAmount; }}Eliminates conditionals: No more scattered if-else chains checking status.
Encapsulates transitions: Each state knows valid transitions and target states.
Easy to add states: New states (e.g., DISPUTED, UNDER_REVIEW) just implement the interface.
Self-documenting: The state diagram directly maps to code structure.
The Problem:
Different membership types have different borrowing limits, loan periods, and fine rates. Our current implementation uses switch statements:
// ❌ Switch statements violate Open/Closed Principle
getBorrowingLimit(): number {
switch (this.membershipType) {
case MembershipType.FACULTY: return 10;
case MembershipType.STUDENT: return 5;
case MembershipType.GENERAL: return 3;
}
}
Adding a new membership type (e.g., RESEARCHER, SENIOR_CITIZEN) requires modifying the Member class in multiple places.
With Strategy Pattern:
Encapsulate membership-specific rules in separate policy classes. Member delegates to its policy without knowing the specifics.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
// ============================================// Strategy Interface// ============================================ /** * Encapsulates all membership-specific rules */interface MembershipPolicy { /** * Returns the type identifier */ getType(): string; /** * Maximum books a member can borrow at once */ getBorrowingLimit(): number; /** * Number of days for loan period */ getLoanPeriodDays(): number; /** * Fine rate per overdue day */ getDailyFineRate(): number; /** * Maximum active reservations */ getMaxReservations(): number; /** * Maximum renewals per loan */ getMaxRenewals(): number; /** * Whether member can access special collections */ canAccessSpecialCollections(): boolean;} // ============================================// Concrete Strategies// ============================================ class StudentMembershipPolicy implements MembershipPolicy { getType(): string { return 'STUDENT'; } getBorrowingLimit(): number { return 5; } getLoanPeriodDays(): number { return 21; } getDailyFineRate(): number { return 0.25; } getMaxReservations(): number { return 5; } getMaxRenewals(): number { return 2; } canAccessSpecialCollections(): boolean { return false; }} class FacultyMembershipPolicy implements MembershipPolicy { getType(): string { return 'FACULTY'; } getBorrowingLimit(): number { return 10; } getLoanPeriodDays(): number { return 30; } getDailyFineRate(): number { return 0.10; } getMaxReservations(): number { return 10; } getMaxRenewals(): number { return 3; } canAccessSpecialCollections(): boolean { return true; }} class GeneralMembershipPolicy implements MembershipPolicy { getType(): string { return 'GENERAL'; } getBorrowingLimit(): number { return 3; } getLoanPeriodDays(): number { return 14; } getDailyFineRate(): number { return 0.50; } getMaxReservations(): number { return 3; } getMaxRenewals(): number { return 1; } canAccessSpecialCollections(): boolean { return false; }} /** * Adding a new membership type is easy! * Just create a new policy class. */class ResearcherMembershipPolicy implements MembershipPolicy { getType(): string { return 'RESEARCHER'; } getBorrowingLimit(): number { return 15; } getLoanPeriodDays(): number { return 60; } getDailyFineRate(): number { return 0.05; } getMaxReservations(): number { return 10; } getMaxRenewals(): number { return 5; } canAccessSpecialCollections(): boolean { return true; }} class SeniorCitizenMembershipPolicy implements MembershipPolicy { getType(): string { return 'SENIOR_CITIZEN'; } getBorrowingLimit(): number { return 5; } getLoanPeriodDays(): number { return 28; } getDailyFineRate(): number { return 0.10; } // Reduced rate getMaxReservations(): number { return 5; } getMaxRenewals(): number { return 3; } canAccessSpecialCollections(): boolean { return false; }} // ============================================// Factory for Creating Policies// ============================================ class MembershipPolicyFactory { private static policies: Map<string, MembershipPolicy> = new Map([ ['STUDENT', new StudentMembershipPolicy()], ['FACULTY', new FacultyMembershipPolicy()], ['GENERAL', new GeneralMembershipPolicy()], ['RESEARCHER', new ResearcherMembershipPolicy()], ['SENIOR_CITIZEN', new SeniorCitizenMembershipPolicy()], ]); static getPolicy(type: string): MembershipPolicy { const policy = this.policies.get(type); if (!policy) { throw new Error(`Unknown membership type: ${type}`); } return policy; } static registerPolicy(type: string, policy: MembershipPolicy): void { this.policies.set(type, policy); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
/** * Member class refactored to use Strategy pattern. * Membership-specific rules delegated to policy object. */class Member { private readonly id: string; private readonly cardNumber: string; private name: string; private email: string; private status: MemberStatus; private readonly activeLoans: Loan[] = []; private readonly reservations: Reservation[] = []; private totalFinesOwed: number = 0; // Strategy pattern: injected policy object private membershipPolicy: MembershipPolicy; constructor( id: string, cardNumber: string, name: string, email: string, membershipType: string ) { this.id = id; this.cardNumber = cardNumber; this.name = name; this.email = email; this.status = MemberStatus.ACTIVE; // Get policy from factory this.membershipPolicy = MembershipPolicyFactory.getPolicy( membershipType ); } // ============ Delegated to Policy ============ getBorrowingLimit(): number { return this.membershipPolicy.getBorrowingLimit(); } getLoanPeriodDays(): number { return this.membershipPolicy.getLoanPeriodDays(); } getDailyFineRate(): number { return this.membershipPolicy.getDailyFineRate(); } getMaxReservations(): number { return this.membershipPolicy.getMaxReservations(); } canAccessSpecialCollections(): boolean { return this.membershipPolicy.canAccessSpecialCollections(); } getMembershipType(): string { return this.membershipPolicy.getType(); } // ============ Policy Can Be Changed ============ /** * Upgrades or changes membership type */ changeMembership(newType: string): void { this.membershipPolicy = MembershipPolicyFactory.getPolicy(newType); // Validate current state against new policy if (this.activeLoans.length > this.getBorrowingLimit()) { // Handle gracefully - don't invalidate existing loans console.warn( `Member ${this.id} has more active loans than new limit` ); } } // ============ Business Logic Unchanged ============ canBorrow(): BorrowEligibility { if (this.status !== MemberStatus.ACTIVE) { return { eligible: false, reason: `Account is ${this.status.toLowerCase()}` }; } // Uses policy's limit if (this.activeLoans.length >= this.getBorrowingLimit()) { return { eligible: false, reason: `At borrowing limit (${this.getBorrowingLimit()} books)` }; } if (this.hasExcessiveFines()) { return { eligible: false, reason: `Outstanding fines exceed threshold` }; } return { eligible: true }; } addReservation(reservation: Reservation): void { // Uses policy's max reservations if (this.reservations.length >= this.getMaxReservations()) { throw new Error( `Maximum reservations (${this.getMaxReservations()}) reached` ); } this.reservations.push(reservation); } // ... rest of Member implementation}We've applied three fundamental design patterns to our Library Management System, each solving a specific problem:
| Pattern | Problem | Solution | Key Benefit |
|---|---|---|---|
| Observer | Notification coupling | Event bus with subscribers | Add notification channels without modifying services |
| State | Complex status logic | State objects per status | State-specific behavior encapsulated; clear transitions |
| Strategy | Type-specific rules | Policy objects per type | Add membership types without modifying Member |
What's Next:
With patterns applied, we now tackle two complex domain challenges: fine calculation and the reservation system. The next page covers:
You've applied Observer, State, and Strategy patterns to transform a functional design into a flexible, maintainable system. Each pattern targets a specific extensibility need: Observer for notification channels, State for status management, Strategy for membership rules. Next, we'll implement the fine calculation and reservation subsystems in detail.