We use cookies to enhance your experience on the site
CodeWorlds

SOLID Principles in JavaScript/TypeScript

Just as ancient architects followed fundamental principles when building durable structures, modern programmers use SOLID principles to create solid, easily extensible code. SOLID is an acronym for five basic object-oriented design principles that, although created for object-oriented languages, work excellently in JavaScript and TypeScript as well.

S - Single Responsibility Principle

Each class or module should have only one reason to change - that is, one clearly defined responsibility.

Bad approach - violating SRP

1// ❌ UserManager class does too many things
2class UserManager {
3  constructor() {
4    this.users = [];
5  }
6
7  // User management
8  addUser(userData) {
9    this.users.push(userData);
10  }
11
12  removeUser(userId) {
13    this.users = this.users.filter(user => user.id !== userId);
14  }
15
16  // Validation - different responsibility
17  validateEmail(email) {
18    return /^[^s@]+@[^s@]+.[^s@]+$/.test(email);
19  }
20
21  validatePassword(password) {
22    return password.length >= 8 && /[A-Z]/.test(password);
23  }
24
25  // Logging - another responsibility
26  logUserAction(action, userId) {
27    console.log(\`[\${new Date().toISOString()}] User \${userId}: \${action}\`);
28  }
29
30  // Sending emails - yet another responsibility
31  sendWelcomeEmail(userEmail) {
32    console.log(\`Sending welcome email to \${userEmail}\`);
33  }
34
35  // Data formatting - another responsibility
36  formatUserData(user) {
37    return \`\${user.firstName} \${user.lastName} (\${user.email})\`;
38  }
39}

Good approach - compliant with SRP

1// ✅ Each class has one responsibility
2
3// User management
4interface User {
5  id: string;
6  firstName: string;
7  lastName: string;
8  email: string;
9  password: string;
10}
11
12class UserRepository {
13  private users: User[] = [];
14
15  add(user: User): void {
16    this.users.push(user);
17  }
18
19  remove(userId: string): boolean {
20    const initialLength = this.users.length;
21    this.users = this.users.filter(user => user.id !== userId);
22    return this.users.length < initialLength;
23  }
24
25  findById(userId: string): User | null {
26    return this.users.find(user => user.id === userId) || null;
27  }
28
29  findByEmail(email: string): User | null {
30    return this.users.find(user => user.email === email) || null;
31  }
32
33  getAll(): User[] {
34    return [...this.users];
35  }
36}
37
38// Validation
39class UserValidator {
40  validateEmail(email: string): boolean {
41    const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
42    return emailRegex.test(email);
43  }
44
45  validatePassword(password: string): boolean {
46    return password.length >= 8 &&
47           /[A-Z]/.test(password) &&
48           /[a-z]/.test(password) &&
49           /[0-9]/.test(password);
50  }
51
52  validateUser(userData: Partial<User>): string[] {
53    const errors: string[] = [];
54
55    if (!userData.firstName || userData.firstName.length < 2) {
56      errors.push('First name must be at least 2 characters');
57    }
58
59    if (!userData.lastName || userData.lastName.length < 2) {
60      errors.push('Last name must be at least 2 characters');
61    }
62
63    if (!userData.email || !this.validateEmail(userData.email)) {
64      errors.push('Invalid email format');
65    }
66
67    if (!userData.password || !this.validatePassword(userData.password)) {
68      errors.push('Password must have at least 8 characters, an uppercase letter, a lowercase letter, and a digit');
69    }
70
71    return errors;
72  }
73}
74
75// Logging
76class UserLogger {
77  private logs: string[] = [];
78
79  logAction(action: string, userId: string): void {
80    const logEntry = \`[\${new Date().toISOString()}] User \${userId}: \${action}\`;
81    this.logs.push(logEntry);
82    console.log(logEntry);
83  }
84
85  getLogs(): string[] {
86    return [...this.logs];
87  }
88
89  clearLogs(): void {
90    this.logs = [];
91  }
92}
93
94// Sending emails
95class EmailService {
96  sendWelcomeEmail(userEmail: string, userName: string): Promise<boolean> {
97    // Simulating email sending
98    console.log(\`Sending welcome email to \${userEmail}\`);
99    return new Promise(resolve => {
100      setTimeout(() => {
101        console.log(\`Welcome email sent to \${userName}\`);
102        resolve(true);
103      }, 1000);
104    });
105  }
106
107  sendPasswordResetEmail(userEmail: string): Promise<boolean> {
108    console.log(\`Sending password reset email to \${userEmail}\`);
109    return Promise.resolve(true);
110  }
111}
112
113// Data formatting
114class UserFormatter {
115  formatFullName(user: User): string {
116    return \`\${user.firstName} \${user.lastName}\`;
117  }
118
119  formatUserDisplay(user: User): string {
120    return \`\${this.formatFullName(user)} (\${user.email})\`;
121  }
122
123  formatUserSummary(user: User): object {
124    return {
125      id: user.id,
126      fullName: this.formatFullName(user),
127      email: user.email
128    };
129  }
130}
131
132// Service layer - coordinates different components
133class UserService {
134  constructor(
135    private userRepository: UserRepository,
136    private userValidator: UserValidator,
137    private userLogger: UserLogger,
138    private emailService: EmailService,
139    private userFormatter: UserFormatter
140  ) {}
141
142  async createUser(userData: Omit<User, 'id'>): Promise<User | null> {
143    // Validation
144    const validationErrors = this.userValidator.validateUser(userData);
145    if (validationErrors.length > 0) {
146      throw new Error(\`Validation errors: \${validationErrors.join(', ')}\`);
147    }
148
149    // Check if email is taken
150    if (this.userRepository.findByEmail(userData.email)) {
151      throw new Error('A user with this email already exists');
152    }
153
154    // Create user
155    const user: User = {
156      id: 'user_' + Math.random().toString(36).substr(2, 9),
157      ...userData
158    };
159
160    this.userRepository.add(user);
161
162    // Logging
163    this.userLogger.logAction('USER_CREATED', user.id);
164
165    // Send welcome email
166    await this.emailService.sendWelcomeEmail(
167      user.email,
168      this.userFormatter.formatFullName(user)
169    );
170
171    return user;
172  }
173
174  deleteUser(userId: string): boolean {
175    const user = this.userRepository.findById(userId);
176    if (!user) {
177      return false;
178    }
179
180    const deleted = this.userRepository.remove(userId);
181    if (deleted) {
182      this.userLogger.logAction('USER_DELETED', userId);
183    }
184
185    return deleted;
186  }
187}
188
189// Usage
190const userRepository = new UserRepository();
191const userValidator = new UserValidator();
192const userLogger = new UserLogger();
193const emailService = new EmailService();
194const userFormatter = new UserFormatter();
195
196const userService = new UserService(
197  userRepository,
198  userValidator,
199  userLogger,
200  emailService,
201  userFormatter
202);
203
204// Test
205userService.createUser({
206  firstName: 'Jan',
207  lastName: 'Kowalski',
208  email: 'jan@example.com',
209  password: 'SecurePass123'
210}).then(user => {
211  if (user) {
212    console.log('User created:', userFormatter.formatUserDisplay(user));
213  }
214});

O - Open/Closed Principle

Classes should be open for extension but closed for modification.

Example with a payment system

1// ✅ Good approach - compliant with OCP
2
3interface PaymentProcessor {
4  processPayment(amount: number, details: any): Promise<PaymentResult>;
5  validatePayment(details: any): boolean;
6}
7
8interface PaymentResult {
9  success: boolean;
10  transactionId?: string;
11  error?: string;
12}
13
14// Basic implementation for credit cards
15class CreditCardProcessor implements PaymentProcessor {
16  async processPayment(amount: number, details: CreditCardDetails): Promise<PaymentResult> {
17    if (!this.validatePayment(details)) {
18      return { success: false, error: 'Invalid card data' };
19    }
20
21    // Simulating card payment processing
22    console.log(\`Processing card payment: \${amount} USD\`);
23
24    return {
25      success: true,
26      transactionId: 'cc_' + Math.random().toString(36).substr(2, 9)
27    };
28  }
29
30  validatePayment(details: CreditCardDetails): boolean {
31    return !!(details.cardNumber &&
32             details.expiryDate &&
33             details.cvv &&
34             details.cardNumber.length === 16);
35  }
36}
37
38interface CreditCardDetails {
39  cardNumber: string;
40  expiryDate: string;
41  cvv: string;
42  holderName: string;
43}
44
45// Extension - new processor without modifying existing code
46class PayPalProcessor implements PaymentProcessor {
47  async processPayment(amount: number, details: PayPalDetails): Promise<PaymentResult> {
48    if (!this.validatePayment(details)) {
49      return { success: false, error: 'Invalid PayPal data' };
50    }
51
52    console.log(\`Processing PayPal payment: \${amount} USD\`);
53
54    return {
55      success: true,
56      transactionId: 'pp_' + Math.random().toString(36).substr(2, 9)
57    };
58  }
59
60  validatePayment(details: PayPalDetails): boolean {
61    return !!(details.email && details.password);
62  }
63}
64
65interface PayPalDetails {
66  email: string;
67  password: string;
68}
69
70// Another extension
71class BankTransferProcessor implements PaymentProcessor {
72  async processPayment(amount: number, details: BankTransferDetails): Promise<PaymentResult> {
73    if (!this.validatePayment(details)) {
74      return { success: false, error: 'Invalid transfer data' };
75    }
76
77    console.log(\`Processing bank transfer: \${amount} USD\`);
78
79    return {
80      success: true,
81      transactionId: 'bt_' + Math.random().toString(36).substr(2, 9)
82    };
83  }
84
85  validatePayment(details: BankTransferDetails): boolean {
86    return !!(details.accountNumber &&
87             details.bankCode &&
88             details.accountNumber.length >= 20);
89  }
90}
91
92interface BankTransferDetails {
93  accountNumber: string;
94  bankCode: string;
95  recipientName: string;
96}
97
98// New processor for cryptocurrencies - without modifying existing code
99class CryptocurrencyProcessor implements PaymentProcessor {
100  async processPayment(amount: number, details: CryptoDetails): Promise<PaymentResult> {
101    if (!this.validatePayment(details)) {
102      return { success: false, error: 'Invalid wallet address' };
103    }
104
105    console.log(\`Processing \${details.currency} payment: \${amount} USD\`);
106
107    return {
108      success: true,
109      transactionId: 'crypto_' + Math.random().toString(36).substr(2, 9)
110    };
111  }
112
113  validatePayment(details: CryptoDetails): boolean {
114    return !!(details.walletAddress &&
115             details.currency &&
116             details.walletAddress.length >= 26);
117  }
118}
119
120interface CryptoDetails {
121  walletAddress: string;
122  currency: 'BTC' | 'ETH' | 'LTC';
123}
124
125// Context using processors - does not need to be modified
126class PaymentService {
127  private processors = new Map<string, PaymentProcessor>();
128
129  registerProcessor(type: string, processor: PaymentProcessor): void {
130    this.processors.set(type, processor);
131  }
132
133  async processPayment(type: string, amount: number, details: any): Promise<PaymentResult> {
134    const processor = this.processors.get(type);
135
136    if (!processor) {
137      return { success: false, error: \`Unsupported payment type: \${type}\` };
138    }
139
140    return processor.processPayment(amount, details);
141  }
142
143  getSupportedPaymentTypes(): string[] {
144    return Array.from(this.processors.keys());
145  }
146}
147
148// Usage - easy to add new payment types
149const paymentService = new PaymentService();
150
151paymentService.registerProcessor('credit-card', new CreditCardProcessor());
152paymentService.registerProcessor('paypal', new PayPalProcessor());
153paymentService.registerProcessor('bank-transfer', new BankTransferProcessor());
154paymentService.registerProcessor('crypto', new CryptocurrencyProcessor());
155
156// Testing different payment types
157async function testPayments() {
158  // Card payment
159  const creditCardResult = await paymentService.processPayment('credit-card', 100, {
160    cardNumber: '1234567890123456',
161    expiryDate: '12/25',
162    cvv: '123',
163    holderName: 'Jan Kowalski'
164  });
165
166  // PayPal payment
167  const paypalResult = await paymentService.processPayment('paypal', 50, {
168    email: 'user@example.com',
169    password: 'password123'
170  });
171
172  // Cryptocurrency payment
173  const cryptoResult = await paymentService.processPayment('crypto', 200, {
174    walletAddress: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
175    currency: 'BTC'
176  });
177
178  console.log('Payment results:', {
179    creditCard: creditCardResult,
180    paypal: paypalResult,
181    crypto: cryptoResult
182  });
183}
184
185testPayments();

L - Liskov Substitution Principle

Objects of derived classes should be able to replace objects of the base class without affecting program correctness.

Example with birds - violating LSP

1// ❌ Violating LSP
2abstract class Bird {
3  abstract fly(): void;
4  abstract makeSound(): void;
5}
6
7class Sparrow extends Bird {
8  fly(): void {
9    console.log('Sparrow is flying!');
10  }
11
12  makeSound(): void {
13    console.log('Chirp chirp!');
14  }
15}
16
17class Eagle extends Bird {
18  fly(): void {
19    console.log('Eagle is soaring!');
20  }
21
22  makeSound(): void {
23    console.log('Screech!');
24  }
25}
26
27// Problem: a penguin cannot fly!
28class Penguin extends Bird {
29  fly(): void {
30    throw new Error('Penguins cannot fly!'); // LSP violation
31  }
32
33  makeSound(): void {
34    console.log('Quack quack!');
35  }
36}
37
38// Function expecting every bird can fly
39function makeBirdFly(bird: Bird): void {
40  bird.fly(); // May throw an error for a penguin!
41}

Corrected approach compliant with LSP

1// ✅ Compliant with LSP - better hierarchy modeling
2
3interface Animal {
4  makeSound(): void;
5  move(): void;
6}
7
8interface Flyable {
9  fly(): void;
10}
11
12interface Swimmable {
13  swim(): void;
14}
15
16interface Walkable {
17  walk(): void;
18}
19
20// Base class for birds
21abstract class Bird implements Animal {
22  abstract makeSound(): void;
23  abstract move(): void;
24
25  eat(): void {
26    console.log('Bird is eating');
27  }
28
29  sleep(): void {
30    console.log('Bird is sleeping');
31  }
32}
33
34// Flying birds
35class FlyingBird extends Bird implements Flyable {
36  makeSound(): void {
37    console.log('Bird makes a sound');
38  }
39
40  move(): void {
41    this.fly();
42  }
43
44  fly(): void {
45    console.log('Bird is flying');
46  }
47}
48
49// Swimming birds
50class SwimmingBird extends Bird implements Swimmable {
51  makeSound(): void {
52    console.log('Bird makes a sound');
53  }
54
55  move(): void {
56    this.swim();
57  }
58
59  swim(): void {
60    console.log('Bird is swimming');
61  }
62}
63
64// Concrete implementations
65class Sparrow extends FlyingBird {
66  makeSound(): void {
67    console.log('Chirp chirp!');
68  }
69
70  fly(): void {
71    console.log('Sparrow flies fast');
72  }
73}
74
75class Eagle extends FlyingBird {
76  makeSound(): void {
77    console.log('Screech!');
78  }
79
80  fly(): void {
81    console.log('Eagle soars majestically');
82  }
83}
84
85class Penguin extends SwimmingBird implements Walkable {
86  makeSound(): void {
87    console.log('Quack quack!');
88  }
89
90  swim(): void {
91    console.log('Penguin swims gracefully');
92  }
93
94  walk(): void {
95    console.log('Penguin walks on ice');
96  }
97
98  move(): void {
99    // Penguin can move in different ways
100    if (Math.random() > 0.5) {
101      this.swim();
102    } else {
103      this.walk();
104    }
105  }
106}
107
108// Functions respecting LSP
109function makeAnimalMove(animal: Animal): void {
110  animal.move(); // Will always work correctly
111}
112
113function makeFlyableObjectFly(flyable: Flyable): void {
114  flyable.fly(); // Only for objects that can actually fly
115}
116
117function makeSwimmableObjectSwim(swimmable: Swimmable): void {
118  swimmable.swim(); // Only for objects that can actually swim
119}
120
121// Testing
122const birds: Bird[] = [
123  new Sparrow(),
124  new Eagle(),
125  new Penguin()
126];
127
128birds.forEach(bird => {
129  bird.makeSound();
130  makeAnimalMove(bird); // Each bird can move in its own way
131});
132
133// Testing flight ability only for flying birds
134const flyingBirds: Flyable[] = [
135  new Sparrow(),
136  new Eagle()
137  // Penguin is not added because it doesn't implement Flyable
138];
139
140flyingBirds.forEach(bird => {
141  makeFlyableObjectFly(bird);
142});

I - Interface Segregation Principle

Classes should not be forced to implement interfaces they don't use.

Violating ISP

1// ❌ Violating ISP - interface too large
2interface MultiFunctionDevice {
3  print(document: string): void;
4  scan(document: string): string;
5  fax(document: string, number: string): void;
6  copy(document: string): string;
7  email(document: string, address: string): void;
8}
9
10// Problem: a simple printer must implement all methods
11class SimplePrinter implements MultiFunctionDevice {
12  print(document: string): void {
13    console.log(\`Printing: \${document}\`);
14  }
15
16  // Forced to implement unused methods
17  scan(document: string): string {
18    throw new Error('Printer does not support scanning');
19  }
20
21  fax(document: string, number: string): void {
22    throw new Error('Printer does not support faxing');
23  }
24
25  copy(document: string): string {
26    throw new Error('Printer does not support copying');
27  }
28
29  email(document: string, address: string): void {
30    throw new Error('Printer does not support emailing');
31  }
32}

Compliant with ISP

1// ✅ Compliant with ISP - segregated interfaces
2
3interface Printer {
4  print(document: string): void;
5}
6
7interface Scanner {
8  scan(document: string): string;
9}
10
11interface FaxMachine {
12  fax(document: string, number: string): void;
13}
14
15interface Copier {
16  copy(document: string): string;
17}
18
19interface EmailSender {
20  email(document: string, address: string): void;
21}
22
23// Simple printer implements only what it needs
24class SimplePrinter implements Printer {
25  print(document: string): void {
26    console.log(\`Printing: \${document}\`);
27  }
28}
29
30// Scanner implements only scanning
31class SimpleScanner implements Scanner {
32  scan(document: string): string {
33    console.log(\`Scanning: \${document}\`);
34    return \`Scanned: \${document}\`;
35  }
36}
37
38// Multi-function device implements needed interfaces
39class MultiFunctionPrinter implements Printer, Scanner, Copier {
40  print(document: string): void {
41    console.log(\`[MFP] Printing: \${document}\`);
42  }
43
44  scan(document: string): string {
45    console.log(\`[MFP] Scanning: \${document}\`);
46    return \`Scanned: \${document}\`;
47  }
48
49  copy(document: string): string {
50    console.log(\`[MFP] Copying: \${document}\`);
51    const scanned = this.scan(document);
52    this.print(scanned);
53    return scanned;
54  }
55}
56
57// Professional office machine
58class OfficeMachine implements Printer, Scanner, FaxMachine, Copier, EmailSender {
59  print(document: string): void {
60    console.log(\`[Office] Printing: \${document}\`);
61  }
62
63  scan(document: string): string {
64    console.log(\`[Office] Scanning: \${document}\`);
65    return \`Scanned: \${document}\`;
66  }
67
68  fax(document: string, number: string): void {
69    console.log(\`[Office] Sending fax \${document} to number \${number}\`);
70  }
71
72  copy(document: string): string {
73    console.log(\`[Office] Copying: \${document}\`);
74    return this.scan(document);
75  }
76
77  email(document: string, address: string): void {
78    console.log(\`[Office] Sending email \${document} to address \${address}\`);
79  }
80}
81
82// Functions using only needed interfaces
83class DocumentProcessor {
84  static printDocument(printer: Printer, document: string): void {
85    printer.print(document);
86  }
87
88  static scanDocument(scanner: Scanner, document: string): string {
89    return scanner.scan(document);
90  }
91
92  static copyDocument(copier: Copier, document: string): string {
93    return copier.copy(document);
94  }
95}
96
97// Usage
98const simplePrinter = new SimplePrinter();
99const simpleScanner = new SimpleScanner();
100const mfp = new MultiFunctionPrinter();
101const officeMachine = new OfficeMachine();
102
103// Each device uses only its capabilities
104DocumentProcessor.printDocument(simplePrinter, 'Document 1');
105DocumentProcessor.scanDocument(simpleScanner, 'Document 2');
106DocumentProcessor.copyDocument(mfp, 'Document 3');
107DocumentProcessor.printDocument(officeMachine, 'Document 4');

D - Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Violating DIP

1// ❌ Violating DIP - depending on concrete implementations
2
3class MySQLDatabase {
4  save(data: any): void {
5    console.log('Saving to MySQL:', data);
6  }
7
8  find(id: string): any {
9    console.log('Searching in MySQL:', id);
10    return { id, data: 'mysql_data' };
11  }
12}
13
14class EmailNotification {
15  send(message: string, recipient: string): void {
16    console.log(\`Sending email to \${recipient}: \${message}\`);
17  }
18}
19
20// Problem: UserService depends on concrete implementations
21class UserService {
22  private database: MySQLDatabase; // Concrete dependency
23  private notification: EmailNotification; // Concrete dependency
24
25  constructor() {
26    this.database = new MySQLDatabase(); // Creating dependencies inside the class
27    this.notification = new EmailNotification();
28  }
29
30  createUser(userData: any): void {
31    this.database.save(userData);
32    this.notification.send('Welcome!', userData.email);
33  }
34
35  getUser(id: string): any {
36    return this.database.find(id);
37  }
38}

Compliant with DIP

1// ✅ Compliant with DIP - depending on abstractions
2
3// Abstractions (interfaces)
4interface Database {
5  save(data: any): void;
6  find(id: string): any;
7  update(id: string, data: any): void;
8  delete(id: string): boolean;
9}
10
11interface NotificationService {
12  send(message: string, recipient: string): Promise<boolean>;
13}
14
15interface Logger {
16  log(level: 'info' | 'warn' | 'error', message: string): void;
17}
18
19// Concrete implementations - low level
20class MySQLDatabase implements Database {
21  save(data: any): void {
22    console.log('Saving to MySQL:', data);
23  }
24
25  find(id: string): any {
26    console.log('Searching in MySQL:', id);
27    return { id, data: 'mysql_data' };
28  }
29
30  update(id: string, data: any): void {
31    console.log(\`Updating in MySQL \${id}:\`, data);
32  }
33
34  delete(id: string): boolean {
35    console.log('Deleting from MySQL:', id);
36    return true;
37  }
38}
39
40class PostgreSQLDatabase implements Database {
41  save(data: any): void {
42    console.log('Saving to PostgreSQL:', data);
43  }
44
45  find(id: string): any {
46    console.log('Searching in PostgreSQL:', id);
47    return { id, data: 'postgres_data' };
48  }
49
50  update(id: string, data: any): void {
51    console.log(\`Updating in PostgreSQL \${id}:\`, data);
52  }
53
54  delete(id: string): boolean {
55    console.log('Deleting from PostgreSQL:', id);
56    return true;
57  }
58}
59
60class MongoDatabase implements Database {
61  save(data: any): void {
62    console.log('Saving to MongoDB:', data);
63  }
64
65  find(id: string): any {
66    console.log('Searching in MongoDB:', id);
67    return { id, data: 'mongo_data' };
68  }
69
70  update(id: string, data: any): void {
71    console.log(\`Updating in MongoDB \${id}:\`, data);
72  }
73
74  delete(id: string): boolean {
75    console.log('Deleting from MongoDB:', id);
76    return true;
77  }
78}
79
80class EmailNotificationService implements NotificationService {
81  async send(message: string, recipient: string): Promise<boolean> {
82    console.log(\`Sending email to \${recipient}: \${message}\`);
83    return true;
84  }
85}
86
87class SMSNotificationService implements NotificationService {
88  async send(message: string, recipient: string): Promise<boolean> {
89    console.log(\`Sending SMS to \${recipient}: \${message}\`);
90    return true;
91  }
92}
93
94class SlackNotificationService implements NotificationService {
95  async send(message: string, recipient: string): Promise<boolean> {
96    console.log(\`Sending Slack message to \${recipient}: \${message}\`);
97    return true;
98  }
99}
100
101class ConsoleLogger implements Logger {
102  log(level: 'info' | 'warn' | 'error', message: string): void {
103    console.log(\`[\${level.toUpperCase()}] \${message}\`);
104  }
105}
106
107class FileLogger implements Logger {
108  log(level: 'info' | 'warn' | 'error', message: string): void {
109    console.log(\`Writing to file [\${level.toUpperCase()}]: \${message}\`);
110  }
111}
112
113// High-level module - depends only on abstractions
114class UserService {
115  constructor(
116    private database: Database,
117    private notificationService: NotificationService,
118    private logger: Logger
119  ) {}
120
121  async createUser(userData: any): Promise<void> {
122    try {
123      this.logger.log('info', \`Creating user: \${userData.email}\`);
124
125      this.database.save(userData);
126
127      await this.notificationService.send(
128        'Welcome to our application!',
129        userData.email
130      );
131
132      this.logger.log('info', \`User \${userData.email} has been created\`);
133    } catch (error) {
134      this.logger.log('error', \`Error creating user: \${error}\`);
135      throw error;
136    }
137  }
138
139  getUser(id: string): any {
140    this.logger.log('info', \`Fetching user: \${id}\`);
141    return this.database.find(id);
142  }
143
144  async updateUser(id: string, userData: any): Promise<void> {
145    try {
146      this.logger.log('info', \`Updating user: \${id}\`);
147
148      this.database.update(id, userData);
149
150      await this.notificationService.send(
151        'Your data has been updated',
152        userData.email
153      );
154
155      this.logger.log('info', \`User \${id} has been updated\`);
156    } catch (error) {
157      this.logger.log('error', \`Error updating user: \${error}\`);
158      throw error;
159    }
160  }
161
162  deleteUser(id: string): boolean {
163    this.logger.log('info', \`Deleting user: \${id}\`);
164    return this.database.delete(id);
165  }
166}
167
168// Dependency Injection Container (simplified)
169class DIContainer {
170  private services = new Map<string, any>();
171
172  register<T>(name: string, instance: T): void {
173    this.services.set(name, instance);
174  }
175
176  get<T>(name: string): T {
177    const service = this.services.get(name);
178    if (!service) {
179      throw new Error(\`Service \${name} not found\`);
180    }
181    return service;
182  }
183}
184
185// Dependency configuration
186const container = new DIContainer();
187
188// Registering different implementations
189container.register<Database>('database', new PostgreSQLDatabase());
190container.register<NotificationService>('notification', new EmailNotificationService());
191container.register<Logger>('logger', new ConsoleLogger());
192
193// Creating the service with injected dependencies
194const userService = new UserService(
195  container.get<Database>('database'),
196  container.get<NotificationService>('notification'),
197  container.get<Logger>('logger')
198);
199
200// Testing
201userService.createUser({
202  id: '1',
203  name: 'Jan Kowalski',
204  email: 'jan@example.com'
205});
206
207// Easy to switch implementations
208const mobileUserService = new UserService(
209  new MongoDatabase(),              // Different database
210  new SMSNotificationService(),     // Different notifications
211  new FileLogger()                  // Different logging
212);
213
214mobileUserService.createUser({
215  id: '2',
216  name: 'Anna Nowak',
217  email: '+48123456789'
218});

Practical example - SOLID-compliant e-commerce system

1// Comprehensive example applying all SOLID principles
2
3// Abstractions
4interface Product {
5  id: string;
6  name: string;
7  price: number;
8  category: string;
9  inStock: boolean;
10}
11
12interface ProductRepository {
13  findById(id: string): Promise<Product | null>;
14  findByCategory(category: string): Promise<Product[]>;
15  save(product: Product): Promise<void>;
16  update(id: string, updates: Partial<Product>): Promise<void>;
17  delete(id: string): Promise<boolean>;
18}
19
20interface PricingStrategy {
21  calculatePrice(product: Product, quantity: number): number;
22}
23
24interface DiscountCalculator {
25  calculateDiscount(product: Product, quantity: number): number;
26}
27
28interface InventoryService {
29  checkAvailability(productId: string, quantity: number): Promise<boolean>;
30  reserveItems(productId: string, quantity: number): Promise<boolean>;
31  releaseReservation(productId: string, quantity: number): Promise<void>;
32}
33
34interface OrderRepository {
35  save(order: Order): Promise<void>;
36  findById(orderId: string): Promise<Order | null>;
37}
38
39interface NotificationService {
40  sendOrderConfirmation(order: Order): Promise<void>;
41  sendInventoryAlert(productId: string): Promise<void>;
42}
43
44// S - Single Responsibility
45class RegularPricingStrategy implements PricingStrategy {
46  calculatePrice(product: Product, quantity: number): number {
47    return product.price * quantity;
48  }
49}
50
51class BulkDiscountStrategy implements PricingStrategy {
52  constructor(private discountCalculator: DiscountCalculator) {}
53
54  calculatePrice(product: Product, quantity: number): number {
55    const basePrice = product.price * quantity;
56    const discount = this.discountCalculator.calculateDiscount(product, quantity);
57    return basePrice - discount;
58  }
59}
60
61class QuantityDiscountCalculator implements DiscountCalculator {
62  calculateDiscount(product: Product, quantity: number): number {
63    if (quantity >= 10) return product.price * quantity * 0.1;
64    if (quantity >= 5) return product.price * quantity * 0.05;
65    return 0;
66  }
67}
68
69class CategoryDiscountCalculator implements DiscountCalculator {
70  private discountRates = new Map([
71    ['electronics', 0.15],
72    ['books', 0.1],
73    ['clothing', 0.2]
74  ]);
75
76  calculateDiscount(product: Product, quantity: number): number {
77    const rate = this.discountRates.get(product.category) || 0;
78    return product.price * quantity * rate;
79  }
80}
81
82// O - Open/Closed - easy to add new strategies
83class SeasonalPricingStrategy implements PricingStrategy {
84  constructor(
85    private basePricingStrategy: PricingStrategy,
86    private seasonalMultiplier: number
87  ) {}
88
89  calculatePrice(product: Product, quantity: number): number {
90    const basePrice = this.basePricingStrategy.calculatePrice(product, quantity);
91    return basePrice * this.seasonalMultiplier;
92  }
93}
94
95// L - Liskov Substitution - all strategies are interchangeable
96class PremiumPricingStrategy implements PricingStrategy {
97  calculatePrice(product: Product, quantity: number): number {
98    return product.price * quantity * 1.2; // 20% premium
99  }
100}
101
102// I - Interface Segregation - small, dedicated interfaces
103interface OrderItem {
104  productId: string;
105  quantity: number;
106  unitPrice: number;
107  totalPrice: number;
108}
109
110interface Order {
111  id: string;
112  customerId: string;
113  items: OrderItem[];
114  totalAmount: number;
115  status: 'pending' | 'confirmed' | 'shipped' | 'delivered';
116  createdAt: Date;
117}
118
119// D - Dependency Inversion
120class OrderService {
121  constructor(
122    private productRepository: ProductRepository,
123    private orderRepository: OrderRepository,
124    private inventoryService: InventoryService,
125    private pricingStrategy: PricingStrategy,
126    private notificationService: NotificationService
127  ) {}
128
129  async createOrder(customerId: string, items: Array<{productId: string, quantity: number}>): Promise<Order> {
130    const orderItems: OrderItem[] = [];
131    let totalAmount = 0;
132
133    // Check availability and calculate prices
134    for (const item of items) {
135      const product = await this.productRepository.findById(item.productId);
136      if (!product) {
137        throw new Error(`Product ${item.productId} not found`);
138      }
139
140      const isAvailable = await this.inventoryService.checkAvailability(item.productId, item.quantity);
141      if (!isAvailable) {
142        throw new Error(`Insufficient quantity of product ${product.name}`);
143      }
144
145      const totalPrice = this.pricingStrategy.calculatePrice(product, item.quantity);
146
147      orderItems.push({
148        productId: item.productId,
149        quantity: item.quantity,
150        unitPrice: product.price,
151        totalPrice
152      });
153
154      totalAmount += totalPrice;
155    }
156
157    // Reserve products
158    for (const item of orderItems) {
159      await this.inventoryService.reserveItems(item.productId, item.quantity);
160    }
161
162    // Create order
163    const order: Order = {
164      id: 'order_' + Math.random().toString(36).substr(2, 9),
165      customerId,
166      items: orderItems,
167      totalAmount,
168      status: 'pending',
169      createdAt: new Date()
170    };
171
172    await this.orderRepository.save(order);
173    await this.notificationService.sendOrderConfirmation(order);
174
175    return order;
176  }
177
178  async confirmOrder(orderId: string): Promise<void> {
179    const order = await this.orderRepository.findById(orderId);
180    if (!order) {
181      throw new Error('Order not found');
182    }
183
184    order.status = 'confirmed';
185    await this.orderRepository.save(order);
186  }
187}
188
189// Example usage with different pricing strategies
190function createOrderServiceWithRegularPricing(): OrderService {
191  return new OrderService(
192    new InMemoryProductRepository(),
193    new InMemoryOrderRepository(),
194    new SimpleInventoryService(),
195    new RegularPricingStrategy(),
196    new EmailNotificationService()
197  );
198}
199
200function createOrderServiceWithBulkDiscount(): OrderService {
201  return new OrderService(
202    new InMemoryProductRepository(),
203    new InMemoryOrderRepository(),
204    new SimpleInventoryService(),
205    new BulkDiscountStrategy(new QuantityDiscountCalculator()),
206    new EmailNotificationService()
207  );
208}
209
210function createOrderServiceWithSeasonalPricing(): OrderService {
211  const basePricing = new BulkDiscountStrategy(new CategoryDiscountCalculator());
212  const seasonalPricing = new SeasonalPricingStrategy(basePricing, 0.8); // 20% discount
213
214  return new OrderService(
215    new InMemoryProductRepository(),
216    new InMemoryOrderRepository(),
217    new SimpleInventoryService(),
218    seasonalPricing,
219    new SlackNotificationService()
220  );
221}
222
223// Helper implementations (simplified)
224class InMemoryProductRepository implements ProductRepository {
225  private products = new Map<string, Product>();
226
227  async findById(id: string): Promise<Product | null> {
228    return this.products.get(id) || null;
229  }
230
231  async findByCategory(category: string): Promise<Product[]> {
232    return Array.from(this.products.values()).filter(p => p.category === category);
233  }
234
235  async save(product: Product): Promise<void> {
236    this.products.set(product.id, product);
237  }
238
239  async update(id: string, updates: Partial<Product>): Promise<void> {
240    const product = this.products.get(id);
241    if (product) {
242      this.products.set(id, { ...product, ...updates });
243    }
244  }
245
246  async delete(id: string): Promise<boolean> {
247    return this.products.delete(id);
248  }
249}
250
251class InMemoryOrderRepository implements OrderRepository {
252  private orders = new Map<string, Order>();
253
254  async save(order: Order): Promise<void> {
255    this.orders.set(order.id, order);
256  }
257
258  async findById(orderId: string): Promise<Order | null> {
259    return this.orders.get(orderId) || null;
260  }
261}
262
263class SimpleInventoryService implements InventoryService {
264  private inventory = new Map<string, number>();
265
266  async checkAvailability(productId: string, quantity: number): Promise<boolean> {
267    const available = this.inventory.get(productId) || 0;
268    return available >= quantity;
269  }
270
271  async reserveItems(productId: string, quantity: number): Promise<boolean> {
272    const available = this.inventory.get(productId) || 0;
273    if (available >= quantity) {
274      this.inventory.set(productId, available - quantity);
275      return true;
276    }
277    return false;
278  }
279
280  async releaseReservation(productId: string, quantity: number): Promise<void> {
281    const current = this.inventory.get(productId) || 0;
282    this.inventory.set(productId, current + quantity);
283  }
284}
285
286class EmailNotificationService implements NotificationService {
287  async sendOrderConfirmation(order: Order): Promise<void> {
288    console.log(`Email: Order confirmation ${order.id} sent`);
289  }
290
291  async sendInventoryAlert(productId: string): Promise<void> {
292    console.log(`Email: Low inventory alert for ${productId}`);
293  }
294}
295
296class SlackNotificationService implements NotificationService {
297  async sendOrderConfirmation(order: Order): Promise<void> {
298    console.log(`Slack: Order confirmation ${order.id} sent`);
299  }
300
301  async sendInventoryAlert(productId: string): Promise<void> {
302    console.log(`Slack: Low inventory alert for ${productId}`);
303  }
304}

Summary of SOLID principles

SOLID principles are the foundation of good software design:

  1. Single Responsibility - one class, one responsibility
  2. Open/Closed - open for extension, closed for modification
  3. Liskov Substitution - derived objects can replace base objects
  4. Interface Segregation - small, dedicated interfaces
  5. Dependency Inversion - depend on abstractions, not implementations

Applying these principles in JavaScript/TypeScript leads to code that is:

  • Easier to maintain - changes in one place don't affect others
  • More testable - ability to mock dependencies
  • Scalable - easy to add new functionalities
  • Readable - clear roles and responsibilities

Just as ancient builders followed architectural principles, modern programmers should follow SOLID principles to create applications that stand the test of time.

Go to CodeWorlds