--- name: solid-principles description: SOLID principles adapted for functional and TypeScript-first development. --- # SOLID Principles for Node.js/TypeScript ## Overview SOLID principles adapted for functional and TypeScript-first development. ## S - Single Responsibility Principle A module/function should have only one reason to change. ### Violation ```typescript // Bad: Does validation, processing, and notification const processOrder = async (order: Order) => { // Validation if (!order.items.length) throw new Error('Empty order'); if (order.total < 0) throw new Error('Invalid total'); // Processing const processed = { ...order, status: 'processed' }; await db.orders.save(processed); // Notification await emailService.send(order.userId, 'Order confirmed'); return processed; }; ``` ### Correct ```typescript // Good: Separate responsibilities const validateOrder = (order: Order): Result => { if (!order.items.length) return Result.fail(emptyOrderError()); if (order.total < 0) return Result.fail(invalidTotalError()); return Result.ok(order); }; const saveOrder = (db: Database) => async (order: Order): Promise => { const processed = { ...order, status: 'processed' }; await db.orders.save(processed); return processed; }; const notifyUser = (notifier: Notifier) => async (userId: string, message: string): Promise => { await notifier.send(userId, message); }; // Compose in orchestrator const processOrder = async (order: Order) => { const validation = validateOrder(order); if (validation.isFailure) return validation; const saved = await saveOrder(db)(validation.value); await notifyUser(emailService)(saved.userId, 'Order confirmed'); return Result.ok(saved); }; ``` ## O - Open/Closed Principle Open for extension, closed for modification. ### Violation ```typescript // Bad: Must modify function to add new discount types const calculateDiscount = (type: string, amount: number): number => { if (type === 'percentage') return amount * 0.1; if (type === 'fixed') return 10; if (type === 'loyalty') return amount * 0.15; return 0; }; ``` ### Correct ```typescript // Good: Extend via new strategies without modifying existing code type DiscountStrategy = (amount: number) => number; const discountStrategies: Record = { percentage: (amount) => amount * 0.1, fixed: () => 10, loyalty: (amount) => amount * 0.15, }; // Easy to extend discountStrategies.holiday = (amount) => amount * 0.25; const calculateDiscount = (type: string, amount: number): number => discountStrategies[type]?.(amount) ?? 0; ``` ## L - Liskov Substitution Principle Subtypes must be substitutable for their base types. ### Violation ```typescript // Bad: Square breaks Rectangle contract class Rectangle { constructor(public width: number, public height: number) {} setWidth(w: number) { this.width = w; } setHeight(h: number) { this.height = h; } area() { return this.width * this.height; } } class Square extends Rectangle { setWidth(w: number) { this.width = w; this.height = w; // Breaks expectation! } } ``` ### Correct ```typescript // Good: Use composition and explicit types type Shape = { area: () => number; }; const createRectangle = (width: number, height: number): Shape => ({ area: () => width * height, }); const createSquare = (side: number): Shape => ({ area: () => side * side, }); ``` ## I - Interface Segregation Principle Clients should not depend on interfaces they don't use. ### Violation ```typescript // Bad: Fat interface interface DataService { read(id: string): Promise; write(data: Data): Promise; delete(id: string): Promise; backup(): Promise; restore(): Promise; migrate(): Promise; } // Client only needs read const reportGenerator = (service: DataService) => { // Only uses service.read(), but depends on entire interface }; ``` ### Correct ```typescript // Good: Segregated interfaces type Reader = { read: (id: string) => Promise; }; type Writer = { write: (data: T) => Promise; }; type Deletable = { delete: (id: string) => Promise; }; // Client depends only on what it needs const reportGenerator = (reader: Reader) => { // Only depends on read capability }; // Compose interfaces as needed type DataService = Reader & Writer & Deletable; ``` ## D - Dependency Inversion Principle Depend on abstractions, not concretions. ### Violation ```typescript // Bad: Direct dependency on implementation import { PrismaClient } from '@prisma/client'; const createUserService = () => { const prisma = new PrismaClient(); // Hardcoded! return { findUser: (id: string) => prisma.user.findFirst({ where: { id } }), }; }; ``` ### Correct ```typescript // Good: Depend on abstraction type UserRepository = { findById: (id: string) => Promise; save: (user: User) => Promise; }; const createUserService = (repo: UserRepository) => ({ findUser: (id: string) => repo.findById(id), createUser: async (data: CreateUserData) => { const user = { id: generateId(), ...data }; return repo.save(user); }, }); // Inject implementation const prismaRepo: UserRepository = { findById: (id) => prisma.user.findFirst({ where: { id } }), save: (user) => prisma.user.create({ data: user }), }; const service = createUserService(prismaRepo); ``` ## SOLID in Practice ### Factory Function Pattern ```typescript // Follows all SOLID principles type Dependencies = { userRepo: UserRepository; orderRepo: OrderRepository; paymentGateway: PaymentGateway; logger: Logger; }; const createOrderProcessor = (deps: Dependencies) => { const validateOrder = (order: Order): Result => { // Single responsibility: validation only }; const processPayment = async (order: Order): Promise> => { // Single responsibility: payment only }; return { process: async (order: Order): Promise> => { const validation = validateOrder(order); if (validation.isFailure) return validation; const payment = await processPayment(validation.value); if (payment.isFailure) return payment; // Compose results return Result.ok({ order: validation.value, payment: payment.value }); }, }; }; ``` ### Testing SOLID Code ```typescript describe('OrderProcessor', () => { it('should process valid order', async () => { // Easy to test due to dependency injection const deps = { userRepo: createFakeUserRepo(), orderRepo: createFakeOrderRepo(), paymentGateway: { charge: jest.fn().mockResolvedValue(Result.ok({})) }, logger: { info: jest.fn() }, }; const processor = createOrderProcessor(deps); const result = await processor.process(createTestOrder()); expect(result.isSuccess).toBe(true); }); }); ```