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 🏛️
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- 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:
- Manages books and members
- Handles book borrowing and returns
- Sends notifications for due dates
- Calculates late fees
- 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:
- Single Responsibility 🎯 - Keep your classes focused on one job
- Open/Closed 🔓🔒 - Design for extension without modification
- Liskov Substitution 🔄 - Ensure your subclasses play nice
- Interface Segregation 🔌 - Keep interfaces lean and relevant
- 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:
- Practice 💪 - Refactor an existing project using SOLID principles
- Design Patterns 🎨 - Explore how SOLID enables classic design patterns
- Architecture 🏗️ - Learn about Clean Architecture and Hexagonal Architecture
- Testing 🧪 - Master unit testing with dependency injection
- Real Projects 🚀 - Apply SOLID in your next TypeScript project
Keep building amazing, maintainable TypeScript applications! You’ve got this! 🎯✨
Happy coding! 🚀👨💻👩💻