+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 276 of 354

📘 SOLID Principles in TypeScript: Clean Architecture

Master solid principles in typescript: clean architecture in TypeScript with practical examples, best practices, and real-world applications 🚀

🚀Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript 📝
  • TypeScript installation ⚡
  • VS Code or preferred IDE 💻

What you'll learn

  • Understand the concept fundamentals 🎯
  • Apply the concept in real projects 🏗️
  • Debug common issues 🐛
  • Write type-safe code ✨

🎯 Introduction

Welcome to the world of SOLID principles in TypeScript! 🌟 If you’ve ever felt like your code was becoming a tangled mess of dependencies, or struggled with making changes without breaking everything, you’re in the right place. Today, we’re going to transform your TypeScript code from a house of cards into a rock-solid fortress! 🏰

SOLID isn’t just another buzzword – it’s your secret weapon for writing maintainable, scalable, and testable code. Think of SOLID principles as the five golden rules that will make your future self (and your teammates) thank you! 💪

📚 Understanding SOLID Principles

SOLID is an acronym that represents five fundamental principles of object-oriented design. Let’s break them down in a way that actually makes sense! 🎓

The Five Pillars of SOLID 🏛️

  1. S - Single Responsibility Principle (SRP)
  2. O - Open/Closed Principle (OCP)
  3. L - Liskov Substitution Principle (LSP)
  4. I - Interface Segregation Principle (ISP)
  5. D - Dependency Inversion Principle (DIP)

Think of these principles like the ingredients in your favorite recipe 🍰 – each one serves a specific purpose, and when combined correctly, they create something amazing!

Why SOLID Matters in TypeScript 🤔

TypeScript’s type system gives us superpowers to implement SOLID principles effectively. With interfaces, generics, and strong typing, we can create architectures that are both flexible and type-safe!

🔧 Basic Syntax and Usage

Let’s dive into each principle with simple, digestible examples!

1. Single Responsibility Principle (SRP) 🎯

A class should have only one reason to change. Think of it like a chef who only cooks – they don’t serve tables or wash dishes!

// ❌ Wrong: Class doing too many things
class User {
  constructor(
    private name: string,
    private email: string
  ) {}

  // User data management
  getName(): string {
    return this.name;
  }

  // Email sending - different responsibility!
  sendEmail(message: string): void {
    // Sending email logic...
    console.log(`Sending "${message}" to ${this.email}`);
  }

  // Database operations - another responsibility!
  save(): void {
    // Database save logic...
    console.log(`Saving user ${this.name} to database`);
  }
}

// ✅ Right: Separated responsibilities
class User {
  constructor(
    private name: string,
    private email: string
  ) {}

  getName(): string {
    return this.name;
  }

  getEmail(): string {
    return this.email;
  }
}

class EmailService {
  send(to: string, message: string): void {
    // 📧 Only handles email sending
    console.log(`Sending "${message}" to ${to}`);
  }
}

class UserRepository {
  save(user: User): void {
    // 💾 Only handles database operations
    console.log(`Saving user ${user.getName()} to database`);
  }
}

2. Open/Closed Principle (OCP) 🔓🔒

Classes should be open for extension but closed for modification. Like a smartphone – you can add apps without changing the phone itself!

// ❌ Wrong: Modifying existing code for new features
class PaymentProcessor {
  processPayment(amount: number, type: string): void {
    if (type === 'credit') {
      console.log(`Processing $${amount} via credit card`);
    } else if (type === 'paypal') {
      console.log(`Processing $${amount} via PayPal`);
    }
    // Adding new payment type requires modifying this method!
  }
}

// ✅ Right: Using abstraction for extension
interface PaymentMethod {
  process(amount: number): void;
}

class CreditCardPayment implements PaymentMethod {
  process(amount: number): void {
    console.log(`💳 Processing $${amount} via credit card`);
  }
}

class PayPalPayment implements PaymentMethod {
  process(amount: number): void {
    console.log(`🅿️ Processing $${amount} via PayPal`);
  }
}

// Easy to add new payment methods!
class CryptoPayment implements PaymentMethod {
  process(amount: number): void {
    console.log(`₿ Processing $${amount} via cryptocurrency`);
  }
}

class PaymentProcessor {
  processPayment(amount: number, method: PaymentMethod): void {
    method.process(amount);
  }
}

3. Liskov Substitution Principle (LSP) 🔄

Derived classes should be substitutable for their base classes. Like different coffee machines – they all make coffee, just in different ways!

// ❌ Wrong: Breaking substitutability
class Bird {
  fly(): void {
    console.log("Flying high! 🦅");
  }
}

class Penguin extends Bird {
  fly(): void {
    // Penguins can't fly! This breaks LSP
    throw new Error("Penguins can't fly!");
  }
}

// ✅ Right: Proper abstraction
interface Bird {
  move(): void;
}

class Eagle implements Bird {
  move(): void {
    console.log("Soaring through the sky! 🦅");
  }
}

class Penguin implements Bird {
  move(): void {
    console.log("Waddling and swimming! 🐧");
  }
}

// Both can be used interchangeably
const birds: Bird[] = [new Eagle(), new Penguin()];
birds.forEach(bird => bird.move()); // Works perfectly!

4. Interface Segregation Principle (ISP) 🔌

Clients shouldn’t depend on interfaces they don’t use. Like a TV remote – you don’t need buttons for features your TV doesn’t have!

// ❌ Wrong: Fat interface
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  code(): void;
}

class Human implements Worker {
  work(): void { console.log("Working hard! 💼"); }
  eat(): void { console.log("Eating lunch! 🍔"); }
  sleep(): void { console.log("Sleeping... 😴"); }
  code(): void { console.log("Writing code! 💻"); }
}

class Robot implements Worker {
  work(): void { console.log("Processing... 🤖"); }
  eat(): void { throw new Error("Robots don't eat!"); }
  sleep(): void { throw new Error("Robots don't sleep!"); }
  code(): void { console.log("Generating code... 🤖"); }
}

// ✅ Right: Segregated interfaces
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

interface Codeable {
  code(): void;
}

class Human implements Workable, Eatable, Sleepable, Codeable {
  work(): void { console.log("Working hard! 💼"); }
  eat(): void { console.log("Eating lunch! 🍔"); }
  sleep(): void { console.log("Sleeping... 😴"); }
  code(): void { console.log("Writing code! 💻"); }
}

class Robot implements Workable, Codeable {
  work(): void { console.log("Processing... 🤖"); }
  code(): void { console.log("Generating code... 🤖"); }
}

5. Dependency Inversion Principle (DIP) 🔀

High-level modules shouldn’t depend on low-level modules. Both should depend on abstractions. Like using standard outlets – your devices don’t care about the power plant!

// ❌ Wrong: Direct dependency on concrete class
class EmailNotification {
  send(message: string): void {
    console.log(`📧 Email: ${message}`);
  }
}

class OrderService {
  private emailNotification = new EmailNotification();

  placeOrder(orderId: string): void {
    // Business logic...
    this.emailNotification.send(`Order ${orderId} placed!`);
  }
}

// ✅ Right: Depending on abstraction
interface NotificationService {
  send(message: string): void;
}

class EmailNotification implements NotificationService {
  send(message: string): void {
    console.log(`📧 Email: ${message}`);
  }
}

class SMSNotification implements NotificationService {
  send(message: string): void {
    console.log(`📱 SMS: ${message}`);
  }
}

class OrderService {
  constructor(private notificationService: NotificationService) {}

  placeOrder(orderId: string): void {
    // Business logic...
    this.notificationService.send(`Order ${orderId} placed!`);
  }
}

// Flexible usage
const emailOrder = new OrderService(new EmailNotification());
const smsOrder = new OrderService(new SMSNotification());

💡 Practical Examples

Let’s build a real-world e-commerce system using SOLID principles! 🛒

Example 1: Shopping Cart System 🛍️

// Following SRP: Each class has one responsibility
interface Product {
  id: string;
  name: string;
  price: number;
}

interface CartItem {
  product: Product;
  quantity: number;
}

// Single Responsibility: Cart only manages items
class ShoppingCart {
  private items: CartItem[] = [];

  addItem(product: Product, quantity: number): void {
    const existingItem = this.items.find(item => item.product.id === product.id);
    
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.items.push({ product, quantity });
    }
  }

  getItems(): CartItem[] {
    return [...this.items];
  }

  getTotal(): number {
    return this.items.reduce(
      (total, item) => total + (item.product.price * item.quantity),
      0
    );
  }
}

// Open/Closed: Extensible discount system
interface DiscountStrategy {
  apply(total: number): number;
}

class PercentageDiscount implements DiscountStrategy {
  constructor(private percentage: number) {}

  apply(total: number): number {
    return total * (1 - this.percentage / 100);
  }
}

class FixedAmountDiscount implements DiscountStrategy {
  constructor(private amount: number) {}

  apply(total: number): number {
    return Math.max(0, total - this.amount);
  }
}

// Dependency Inversion: PriceCalculator depends on abstractions
class PriceCalculator {
  constructor(private discountStrategy?: DiscountStrategy) {}

  calculateFinalPrice(cart: ShoppingCart): number {
    let total = cart.getTotal();
    
    if (this.discountStrategy) {
      total = this.discountStrategy.apply(total);
    }
    
    return total;
  }
}

// Usage example
const cart = new ShoppingCart();
cart.addItem({ id: '1', name: 'TypeScript Book', price: 39.99 }, 2);
cart.addItem({ id: '2', name: 'Coffee Mug', price: 12.99 }, 1);

// 20% off discount
const calculator = new PriceCalculator(new PercentageDiscount(20));
console.log(`Total: $${calculator.calculateFinalPrice(cart).toFixed(2)} 🎉`);

Example 2: User Authentication System 🔐

// Interface Segregation: Different auth methods
interface Authenticatable {
  authenticate(credentials: any): Promise<boolean>;
}

interface TwoFactorAuthenticatable {
  sendTwoFactorCode(): Promise<void>;
  verifyTwoFactorCode(code: string): Promise<boolean>;
}

// Concrete implementations
class PasswordAuth implements Authenticatable {
  async authenticate(credentials: { username: string; password: string }): Promise<boolean> {
    // 🔑 Password verification logic
    console.log(`Checking password for ${credentials.username}`);
    return true; // Simplified
  }
}

class BiometricAuth implements Authenticatable {
  async authenticate(credentials: { biometricData: string }): Promise<boolean> {
    // 👆 Biometric verification logic
    console.log('Verifying biometric data');
    return true; // Simplified
  }
}

class GoogleAuth implements Authenticatable, TwoFactorAuthenticatable {
  async authenticate(credentials: { token: string }): Promise<boolean> {
    // 🔷 OAuth verification
    console.log('Verifying Google token');
    return true;
  }

  async sendTwoFactorCode(): Promise<void> {
    console.log('📱 Sending 2FA code via SMS');
  }

  async verifyTwoFactorCode(code: string): Promise<boolean> {
    console.log(`Verifying code: ${code}`);
    return true;
  }
}

// Dependency Inversion: AuthService depends on abstractions
class AuthService {
  constructor(
    private authMethod: Authenticatable,
    private logger?: Logger
  ) {}

  async login(credentials: any): Promise<boolean> {
    try {
      const isAuthenticated = await this.authMethod.authenticate(credentials);
      
      if (isAuthenticated) {
        this.logger?.log('✅ Authentication successful');
      } else {
        this.logger?.log('❌ Authentication failed');
      }
      
      return isAuthenticated;
    } catch (error) {
      this.logger?.log('🚨 Authentication error');
      return false;
    }
  }
}

// Logger abstraction
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[AUTH LOG] ${message}`);
  }
}

// Usage
const passwordAuthService = new AuthService(
  new PasswordAuth(),
  new ConsoleLogger()
);

const googleAuthService = new AuthService(
  new GoogleAuth(),
  new ConsoleLogger()
);

Example 3: Game Character System 🎮

// Base character interface following LSP
interface GameCharacter {
  name: string;
  health: number;
  move(x: number, y: number): void;
  takeDamage(amount: number): void;
}

// Segregated interfaces for abilities
interface MeleeAttacker {
  meleeAttack(target: GameCharacter): void;
}

interface RangedAttacker {
  rangedAttack(target: GameCharacter, distance: number): void;
}

interface Healer {
  heal(target: GameCharacter): void;
}

interface MagicUser {
  castSpell(spellName: string, target?: GameCharacter): void;
}

// Character implementations
class Warrior implements GameCharacter, MeleeAttacker {
  constructor(
    public name: string,
    public health: number = 100
  ) {}

  move(x: number, y: number): void {
    console.log(`⚔️ ${this.name} charges to (${x}, ${y})`);
  }

  takeDamage(amount: number): void {
    this.health = Math.max(0, this.health - amount);
    console.log(`💔 ${this.name} takes ${amount} damage! Health: ${this.health}`);
  }

  meleeAttack(target: GameCharacter): void {
    console.log(`⚔️ ${this.name} slashes at ${target.name}!`);
    target.takeDamage(20);
  }
}

class Archer implements GameCharacter, RangedAttacker {
  constructor(
    public name: string,
    public health: number = 80
  ) {}

  move(x: number, y: number): void {
    console.log(`🏹 ${this.name} moves stealthily to (${x}, ${y})`);
  }

  takeDamage(amount: number): void {
    this.health = Math.max(0, this.health - amount);
    console.log(`💔 ${this.name} takes ${amount} damage! Health: ${this.health}`);
  }

  rangedAttack(target: GameCharacter, distance: number): void {
    if (distance <= 50) {
      console.log(`🏹 ${this.name} shoots an arrow at ${target.name}!`);
      target.takeDamage(15);
    } else {
      console.log(`❌ ${target.name} is too far away!`);
    }
  }
}

class Cleric implements GameCharacter, Healer, MagicUser {
  constructor(
    public name: string,
    public health: number = 70
  ) {}

  move(x: number, y: number): void {
    console.log(`✨ ${this.name} glides to (${x}, ${y})`);
  }

  takeDamage(amount: number): void {
    this.health = Math.max(0, this.health - amount);
    console.log(`💔 ${this.name} takes ${amount} damage! Health: ${this.health}`);
  }

  heal(target: GameCharacter): void {
    const healAmount = 25;
    target.health = Math.min(100, target.health + healAmount);
    console.log(`💚 ${this.name} heals ${target.name} for ${healAmount}! New health: ${target.health}`);
  }

  castSpell(spellName: string, target?: GameCharacter): void {
    console.log(`✨ ${this.name} casts ${spellName}!`);
    if (target && spellName === 'Holy Light') {
      target.takeDamage(10);
    }
  }
}

// Combat system using dependency injection
class CombatSystem {
  private combatLog: string[] = [];

  constructor(private logger?: Logger) {}

  executeAttack(attacker: any, target: GameCharacter): void {
    if ('meleeAttack' in attacker) {
      attacker.meleeAttack(target);
    } else if ('rangedAttack' in attacker) {
      attacker.rangedAttack(target, 30);
    }
    
    this.logger?.log(`Combat: ${attacker.name} attacked ${target.name}`);
  }
}

// Game example
const warrior = new Warrior('Thorin');
const archer = new Archer('Legolas');
const cleric = new Cleric('Gandalf');

const combat = new CombatSystem(new ConsoleLogger());
combat.executeAttack(warrior, archer);
cleric.heal(archer);

🚀 Advanced Concepts

Combining SOLID with TypeScript Features 🎯

TypeScript’s advanced features make SOLID principles even more powerful:

// Using generics for flexible, type-safe abstractions
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  save(entity: T): Promise<void>;
  delete(id: string): Promise<void>;
}

// Type-safe implementation
class InMemoryRepository<T extends { id: string }> implements Repository<T> {
  private storage = new Map<string, T>();

  async findById(id: string): Promise<T | null> {
    return this.storage.get(id) || null;
  }

  async save(entity: T): Promise<void> {
    this.storage.set(entity.id, entity);
  }

  async delete(id: string): Promise<void> {
    this.storage.delete(id);
  }
}

// Using conditional types for smart interfaces
type AsyncFunction<T> = T extends (...args: any[]) => any
  ? (...args: Parameters<T>) => Promise<ReturnType<T>>
  : never;

// Decorator pattern with TypeScript
function Cached<T extends { new(...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    private cache = new Map<string, any>();

    async getData(key: string): Promise<any> {
      if (this.cache.has(key)) {
        console.log('📦 Returning from cache');
        return this.cache.get(key);
      }

      const result = await super.getData(key);
      this.cache.set(key, result);
      return result;
    }
  };
}

Event-Driven Architecture with SOLID 🎪

// Event system following SOLID principles
interface Event {
  type: string;
  timestamp: Date;
}

interface OrderEvent extends Event {
  orderId: string;
  userId: string;
}

interface EventHandler<T extends Event> {
  handle(event: T): Promise<void>;
}

// Event bus with dependency injection
class EventBus {
  private handlers = new Map<string, EventHandler<any>[]>();

  register<T extends Event>(eventType: string, handler: EventHandler<T>): void {
    const handlers = this.handlers.get(eventType) || [];
    handlers.push(handler);
    this.handlers.set(eventType, handlers);
  }

  async emit<T extends Event>(event: T): Promise<void> {
    const handlers = this.handlers.get(event.type) || [];
    
    await Promise.all(
      handlers.map(handler => handler.handle(event))
    );
  }
}

// Concrete handlers
class EmailOrderHandler implements EventHandler<OrderEvent> {
  constructor(private emailService: NotificationService) {}

  async handle(event: OrderEvent): Promise<void> {
    await this.emailService.send(
      `Order ${event.orderId} confirmed! 🎉`
    );
  }
}

class InventoryOrderHandler implements EventHandler<OrderEvent> {
  async handle(event: OrderEvent): Promise<void> {
    console.log(`📦 Updating inventory for order ${event.orderId}`);
    // Inventory update logic
  }
}

// Usage
const eventBus = new EventBus();
const emailService = new EmailNotification();

eventBus.register('order.placed', new EmailOrderHandler(emailService));
eventBus.register('order.placed', new InventoryOrderHandler());

eventBus.emit({
  type: 'order.placed',
  timestamp: new Date(),
  orderId: '12345',
  userId: 'user-789'
});

⚠️ Common Pitfalls and Solutions

Pitfall 1: Over-Engineering 🏗️

// ❌ Too abstract for simple use case
interface StringProcessor {
  process(input: string): string;
}

class UpperCaseProcessor implements StringProcessor {
  process(input: string): string {
    return input.toUpperCase();
  }
}

class StringService {
  constructor(private processor: StringProcessor) {}
  
  processString(input: string): string {
    return this.processor.process(input);
  }
}

// ✅ Keep it simple when appropriate
const toUpperCase = (input: string): string => input.toUpperCase();

Pitfall 2: Anemic Domain Models 🦴

// ❌ No behavior, just data
class User {
  id: string;
  email: string;
  password: string;
}

class UserService {
  validateEmail(user: User): boolean {
    return user.email.includes('@');
  }
}

// ✅ Rich domain model
class User {
  constructor(
    private id: string,
    private email: string,
    private password: string
  ) {}

  isValidEmail(): boolean {
    return this.email.includes('@');
  }

  changePassword(oldPassword: string, newPassword: string): void {
    if (this.password !== oldPassword) {
      throw new Error('Invalid password');
    }
    this.password = newPassword;
  }
}

Pitfall 3: Misunderstanding LSP 🔄

// ❌ Violating LSP
class Rectangle {
  constructor(
    protected width: number,
    protected height: number
  ) {}

  setWidth(width: number): void {
    this.width = width;
  }

  setHeight(height: number): void {
    this.height = height;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width: number): void {
    this.width = width;
    this.height = width; // Breaks LSP!
  }

  setHeight(height: number): void {
    this.width = height;
    this.height = height; // Breaks LSP!
  }
}

// ✅ Proper abstraction
interface Shape {
  getArea(): number;
}

class Rectangle implements Shape {
  constructor(
    private width: number,
    private height: number
  ) {}

  getArea(): number {
    return this.width * this.height;
  }
}

class Square implements Shape {
  constructor(private side: number) {}

  getArea(): number {
    return this.side * this.side;
  }
}

🛠️ Best Practices

1. Start Simple, Refactor to SOLID 🌱

// Start with working code
class OrderProcessor {
  processOrder(order: Order): void {
    // Validate
    if (!order.items.length) {
      throw new Error('Order must have items');
    }

    // Calculate total
    const total = order.items.reduce((sum, item) => sum + item.price, 0);

    // Send email
    console.log(`Order total: $${total}`);
  }
}

// Refactor when complexity grows
class OrderValidator {
  validate(order: Order): void {
    if (!order.items.length) {
      throw new Error('Order must have items');
    }
  }
}

class PriceCalculator {
  calculate(items: OrderItem[]): number {
    return items.reduce((sum, item) => sum + item.price, 0);
  }
}

class OrderProcessor {
  constructor(
    private validator: OrderValidator,
    private calculator: PriceCalculator,
    private notifier: NotificationService
  ) {}

  processOrder(order: Order): void {
    this.validator.validate(order);
    const total = this.calculator.calculate(order.items);
    this.notifier.send(`Order total: $${total}`);
  }
}

2. Use TypeScript Features Wisely 🎓

// Leverage TypeScript for better SOLID
type Constructor<T = {}> = new (...args: any[]) => T;

// Mixins for flexible composition
function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = new Date();
    
    updateTimestamp(): void {
      this.timestamp = new Date();
    }
  };
}

function Tagged<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    tags: string[] = [];
    
    addTag(tag: string): void {
      this.tags.push(tag);
    }
  };
}

// Compose behaviors
class Article {
  constructor(public title: string) {}
}

const TimestampedTaggedArticle = Tagged(Timestamped(Article));
const article = new TimestampedTaggedArticle('SOLID in TypeScript');
article.addTag('architecture');
article.updateTimestamp();

3. Test-Driven SOLID 🧪

// Design for testability
interface Clock {
  now(): Date;
}

class SystemClock implements Clock {
  now(): Date {
    return new Date();
  }
}

class TestClock implements Clock {
  constructor(private fixedDate: Date) {}
  
  now(): Date {
    return this.fixedDate;
  }
}

class AuditLogger {
  constructor(
    private clock: Clock,
    private storage: Repository<AuditLog>
  ) {}

  log(action: string, userId: string): void {
    const entry: AuditLog = {
      id: generateId(),
      action,
      userId,
      timestamp: this.clock.now()
    };
    
    this.storage.save(entry);
  }
}

// Easy to test!
describe('AuditLogger', () => {
  it('should log with correct timestamp', () => {
    const fixedDate = new Date('2024-01-01');
    const clock = new TestClock(fixedDate);
    const storage = new InMemoryRepository<AuditLog>();
    const logger = new AuditLogger(clock, storage);
    
    logger.log('LOGIN', 'user123');
    
    // Assert...
  });
});

🧪 Hands-On Exercise

Time to put your SOLID knowledge to the test! 🎯 Build a library management system that follows all SOLID principles.

Challenge: Library Management System 📚

Create a system that:

  1. Manages books and members
  2. Handles book borrowing and returns
  3. Sends notifications for due dates
  4. Calculates late fees
  5. Generates reports

Start with this skeleton:

// Your interfaces here
interface Book {
  isbn: string;
  title: string;
  author: string;
  available: boolean;
}

interface Member {
  id: string;
  name: string;
  email: string;
}

// Implement your SOLID solution!
🔍 Click to see the solution
// Domain entities
interface Book {
  isbn: string;
  title: string;
  author: string;
  available: boolean;
}

interface Member {
  id: string;
  name: string;
  email: string;
}

interface Loan {
  id: string;
  bookIsbn: string;
  memberId: string;
  loanDate: Date;
  dueDate: Date;
  returnDate?: Date;
}

// Repository interfaces (DIP)
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<void>;
  update(entity: T): Promise<void>;
}

// Service interfaces (ISP)
interface NotificationService {
  sendNotification(to: string, message: string): Promise<void>;
}

interface FeeCalculator {
  calculateLateFee(daysLate: number): number;
}

interface ReportGenerator {
  generateReport(type: string, data: any): string;
}

// Concrete implementations
class EmailNotificationService implements NotificationService {
  async sendNotification(to: string, message: string): Promise<void> {
    console.log(`📧 Sending email to ${to}: ${message}`);
  }
}

class StandardFeeCalculator implements FeeCalculator {
  private dailyFee = 0.50;

  calculateLateFee(daysLate: number): number {
    return Math.max(0, daysLate * this.dailyFee);
  }
}

// Core services following SRP
class BookService {
  constructor(private bookRepository: Repository<Book>) {}

  async findAvailableBooks(): Promise<Book[]> {
    const books = await this.bookRepository.findAll();
    return books.filter(book => book.available);
  }

  async markAsLoaned(isbn: string): Promise<void> {
    const book = await this.bookRepository.findById(isbn);
    if (book) {
      book.available = false;
      await this.bookRepository.update(book);
    }
  }

  async markAsReturned(isbn: string): Promise<void> {
    const book = await this.bookRepository.findById(isbn);
    if (book) {
      book.available = true;
      await this.bookRepository.update(book);
    }
  }
}

class LoanService {
  constructor(
    private loanRepository: Repository<Loan>,
    private bookService: BookService,
    private notificationService: NotificationService,
    private feeCalculator: FeeCalculator
  ) {}

  async borrowBook(memberId: string, bookIsbn: string): Promise<void> {
    const loanDate = new Date();
    const dueDate = new Date();
    dueDate.setDate(dueDate.getDate() + 14); // 2 weeks loan

    const loan: Loan = {
      id: this.generateId(),
      bookIsbn,
      memberId,
      loanDate,
      dueDate
    };

    await this.loanRepository.save(loan);
    await this.bookService.markAsLoaned(bookIsbn);
    
    await this.notificationService.sendNotification(
      memberId,
      `Book borrowed! Due date: ${dueDate.toLocaleDateString()}`
    );
  }

  async returnBook(loanId: string): Promise<number> {
    const loan = await this.loanRepository.findById(loanId);
    if (!loan) throw new Error('Loan not found');

    loan.returnDate = new Date();
    await this.loanRepository.update(loan);
    await this.bookService.markAsReturned(loan.bookIsbn);

    const daysLate = this.calculateDaysLate(loan.dueDate, loan.returnDate);
    const fee = this.feeCalculator.calculateLateFee(daysLate);

    if (fee > 0) {
      await this.notificationService.sendNotification(
        loan.memberId,
        `Book returned. Late fee: $${fee.toFixed(2)}`
      );
    }

    return fee;
  }

  private calculateDaysLate(dueDate: Date, returnDate: Date): number {
    const diff = returnDate.getTime() - dueDate.getTime();
    return Math.max(0, Math.floor(diff / (1000 * 60 * 60 * 24)));
  }

  private generateId(): string {
    return `loan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

// Report generation following OCP
abstract class Report {
  abstract generate(data: any): string;
}

class OverdueReport extends Report {
  generate(loans: Loan[]): string {
    const overdue = loans.filter(loan => 
      !loan.returnDate && new Date() > loan.dueDate
    );
    
    return `📊 Overdue Books Report\n` +
           `Total overdue: ${overdue.length}\n` +
           overdue.map(loan => 
             `- Loan ${loan.id}: Due ${loan.dueDate.toLocaleDateString()}`
           ).join('\n');
  }
}

class PopularBooksReport extends Report {
  generate(loanHistory: Loan[]): string {
    const bookCounts = new Map<string, number>();
    
    loanHistory.forEach(loan => {
      const count = bookCounts.get(loan.bookIsbn) || 0;
      bookCounts.set(loan.bookIsbn, count + 1);
    });
    
    const sorted = Array.from(bookCounts.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10);
    
    return `📊 Popular Books Report\n` +
           sorted.map(([isbn, count]) => 
             `- ISBN ${isbn}: ${count} loans`
           ).join('\n');
  }
}

// Usage example
class LibraryFacade {
  constructor(
    private loanService: LoanService,
    private reportGenerators: Map<string, Report>
  ) {}

  async borrowBook(memberId: string, bookIsbn: string): Promise<void> {
    await this.loanService.borrowBook(memberId, bookIsbn);
    console.log(`✅ Book ${bookIsbn} borrowed by member ${memberId}`);
  }

  async returnBook(loanId: string): Promise<void> {
    const fee = await this.loanService.returnBook(loanId);
    console.log(`✅ Book returned. Fee: $${fee.toFixed(2)}`);
  }

  generateReport(type: string, data: any): string {
    const generator = this.reportGenerators.get(type);
    if (!generator) {
      throw new Error(`Unknown report type: ${type}`);
    }
    return generator.generate(data);
  }
}

// 🎉 Congratulations! You've built a SOLID library system!

🎓 Key Takeaways

You’ve just mastered the SOLID principles in TypeScript! 🎉 Here’s what you’ve learned:

  1. Single Responsibility 🎯 - Keep your classes focused on one job
  2. Open/Closed 🔓🔒 - Design for extension without modification
  3. Liskov Substitution 🔄 - Ensure your subclasses play nice
  4. Interface Segregation 🔌 - Keep interfaces lean and relevant
  5. Dependency Inversion 🔀 - Depend on abstractions, not concretions

Remember:

  • SOLID principles are guidelines, not rigid rules
  • Start simple and refactor to SOLID as complexity grows
  • TypeScript’s type system is your best friend for SOLID design
  • Always consider the context and avoid over-engineering

🤝 Next Steps

Congratulations on completing this SOLID journey! 🌟 Here are your next adventures:

  1. Practice 💪 - Refactor an existing project using SOLID principles
  2. Design Patterns 🎨 - Explore how SOLID enables classic design patterns
  3. Architecture 🏗️ - Learn about Clean Architecture and Hexagonal Architecture
  4. Testing 🧪 - Master unit testing with dependency injection
  5. Real Projects 🚀 - Apply SOLID in your next TypeScript project

Keep building amazing, maintainable TypeScript applications! You’ve got this! 🎯✨

Happy coding! 🚀👨‍💻👩‍💻