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.
Each class or module should have only one reason to change - that is, one clearly defined responsibility.
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}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});Classes should be open for extension but closed for modification.
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();Objects of derived classes should be able to replace objects of the base class without affecting program correctness.
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}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});Classes should not be forced to implement interfaces they don't use.
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}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');High-level modules should not depend on low-level modules. Both should depend on abstractions.
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}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});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}SOLID principles are the foundation of good software design:
Applying these principles in JavaScript/TypeScript leads to code that is:
Just as ancient builders followed architectural principles, modern programmers should follow SOLID principles to create applications that stand the test of time.