Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Zasady SOLID w JavaScript/TypeScript

Podobnie jak starożytni architekci przestrzegali fundamentalnych zasad przy budowie trwałych struktur, współcześni programiści używają zasad SOLID do tworzenia solidnego, łatwo rozszerzalnego kodu. SOLID to akronim pięciu podstawowych zasad projektowania obiektowego, które choć powstały dla języków obiektowych, doskonale sprawdzają się również w JavaScript i TypeScript.

S - Single Responsibility Principle (Zasada jednej odpowiedzialności)

Każda klasa lub moduł powinien mieć tylko jeden powód do zmiany - czyli jedną, jasno określoną odpowiedzialność.

Złe podejście - naruszenie SRP

1// ❌ Klasa UserManager robi za dużo rzeczy
2class UserManager {
3  constructor() {
4    this.users = [];
5  }
6  
7  // Zarządzanie użytkownikami
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  // Walidacja - inna odpowiedzialność
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  // Logowanie - kolejna odpowiedzialność
26  logUserAction(action, userId) {
27    console.log(`[${new Date().toISOString()}] User ${userId}: ${action}`);
28  }
29  
30  // Wysyłanie emaili - jeszcze inna odpowiedzialność
31  sendWelcomeEmail(userEmail) {
32    console.log(`Sending welcome email to ${userEmail}`);
33  }
34  
35  // Formatowanie danych - kolejna odpowiedzialność
36  formatUserData(user) {
37    return `${user.firstName} ${user.lastName} (${user.email})`;
38  }
39}

Dobre podejście - zgodne z SRP

1// ✅ Każda klasa ma jedną odpowiedzialność
2
3// Zarządzanie użytkownikami
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// Walidacja
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('Imię musi mieć co najmniej 2 znaki');
57    }
58    
59    if (!userData.lastName || userData.lastName.length < 2) {
60      errors.push('Nazwisko musi mieć co najmniej 2 znaki');
61    }
62    
63    if (!userData.email || !this.validateEmail(userData.email)) {
64      errors.push('Nieprawidłowy format email');
65    }
66    
67    if (!userData.password || !this.validatePassword(userData.password)) {
68      errors.push('Hasło musi mieć co najmniej 8 znaków, wielką literę, małą literę i cyfrę');
69    }
70    
71    return errors;
72  }
73}
74
75// Logowanie
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// Wysyłanie emaili
95class EmailService {
96  sendWelcomeEmail(userEmail: string, userName: string): Promise<boolean> {
97    // Symulacja wysyłania emaila
98    console.log(`Wysyłanie maila powitalnego do ${userEmail}`);
99    return new Promise(resolve => {
100      setTimeout(() => {
101        console.log(`Mail powitalny wysłany do ${userName}`);
102        resolve(true);
103      }, 1000);
104    });
105  }
106  
107  sendPasswordResetEmail(userEmail: string): Promise<boolean> {
108    console.log(`Wysyłanie maila z resetem hasła do ${userEmail}`);
109    return Promise.resolve(true);
110  }
111}
112
113// Formatowanie danych
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 - koordynuje działanie różnych komponentów
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    // Walidacja
144    const validationErrors = this.userValidator.validateUser(userData);
145    if (validationErrors.length > 0) {
146      throw new Error(`Błędy walidacji: ${validationErrors.join(', ')}`);
147    }
148    
149    // Sprawdzenie czy email nie jest zajęty
150    if (this.userRepository.findByEmail(userData.email)) {
151      throw new Error('Użytkownik z tym emailem już istnieje');
152    }
153    
154    // Tworzenie użytkownika
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    // Logowanie
163    this.userLogger.logAction('USER_CREATED', user.id);
164    
165    // Wysyłanie maila powitalnego
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// Użycie
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('Utworzono użytkownika:', userFormatter.formatUserDisplay(user));
213  }
214});

O - Open/Closed Principle (Zasada otwarte/zamknięte)

Klasy powinny być otwarte na rozszerzenie, ale zamknięte na modyfikację.

Przykład z systemem płatności

1// ✅ Dobre podejście - zgodne z 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// Podstawowa implementacja dla kart kredytowych
15class CreditCardProcessor implements PaymentProcessor {
16  async processPayment(amount: number, details: CreditCardDetails): Promise<PaymentResult> {
17    if (!this.validatePayment(details)) {
18      return { success: false, error: 'Nieprawidłowe dane karty' };
19    }
20    
21    // Symulacja przetwarzania płatności kartą
22    console.log(`Przetwarzanie płatności kartą: ${amount} PLN`);
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// Rozszerzenie - nowy procesor bez modyfikacji istniejącego kodu
46class PayPalProcessor implements PaymentProcessor {
47  async processPayment(amount: number, details: PayPalDetails): Promise<PaymentResult> {
48    if (!this.validatePayment(details)) {
49      return { success: false, error: 'Nieprawidłowe dane PayPal' };
50    }
51    
52    console.log(`Przetwarzanie płatności PayPal: ${amount} PLN`);
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// Kolejne rozszerzenie
71class BankTransferProcessor implements PaymentProcessor {
72  async processPayment(amount: number, details: BankTransferDetails): Promise<PaymentResult> {
73    if (!this.validatePayment(details)) {
74      return { success: false, error: 'Nieprawidłowe dane przelewu' };
75    }
76    
77    console.log(`Przetwarzanie przelewu bankowego: ${amount} PLN`);
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// Nowy procesor dla kryptowalut - bez modyfikacji istniejącego kodu
99class CryptocurrencyProcessor implements PaymentProcessor {
100  async processPayment(amount: number, details: CryptoDetails): Promise<PaymentResult> {
101    if (!this.validatePayment(details)) {
102      return { success: false, error: 'Nieprawidłowy adres wallet' };
103    }
104    
105    console.log(`Przetwarzanie płatności ${details.currency}: ${amount} PLN`);
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// Kontekst używający procesorów - nie musi być modyfikowany
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: `Nieobsługiwany typ płatności: ${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// Użycie - łatwe dodawanie nowych typów płatności
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// Testowanie różnych typów płatności
157async function testPayments() {
158  // Płatność kartą
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  // Płatność PayPal
167  const paypalResult = await paymentService.processPayment('paypal', 50, {
168    email: 'user@example.com',
169    password: 'password123'
170  });
171  
172  // Płatność kryptowalutą
173  const cryptoResult = await paymentService.processPayment('crypto', 200, {
174    walletAddress: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
175    currency: 'BTC'
176  });
177  
178  console.log('Wyniki płatności:', {
179    creditCard: creditCardResult,
180    paypal: paypalResult,
181    crypto: cryptoResult
182  });
183}
184
185testPayments();

L - Liskov Substitution Principle (Zasada podstawienia Liskov)

Obiekty klas pochodnych powinny móc zastąpić obiekty klasy bazowej bez wpływu na poprawność programu.

Przykład z ptakami - naruszenie LSP

1// ❌ Naruszenie LSP
2abstract class Bird {
3  abstract fly(): void;
4  abstract makeSound(): void;
5}
6
7class Sparrow extends Bird {
8  fly(): void {
9    console.log('Wróbel leci!');
10  }
11  
12  makeSound(): void {
13    console.log('Ćwir ćwir!');
14  }
15}
16
17class Eagle extends Bird {
18  fly(): void {
19    console.log('Orzeł szybuje!');
20  }
21  
22  makeSound(): void {
23    console.log('Krra!');
24  }
25}
26
27// Problem: pingwin nie potrafi latać!
28class Penguin extends Bird {
29  fly(): void {
30    throw new Error('Pingwiny nie potrafią latać!'); // Naruszenie LSP
31  }
32  
33  makeSound(): void {
34    console.log('Kwa kwa!');
35  }
36}
37
38// Funkcja oczekująca, że każdy ptak potrafi latać
39function makeBirdFly(bird: Bird): void {
40  bird.fly(); // Może rzucić błąd dla pingwina!
41}

Poprawione podejście zgodne z LSP

1// ✅ Zgodne z LSP - lepsze modelowanie hierarchii
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// Klasa bazowa dla ptaków
21abstract class Bird implements Animal {
22  abstract makeSound(): void;
23  abstract move(): void;
24  
25  eat(): void {
26    console.log('Ptak je');
27  }
28  
29  sleep(): void {
30    console.log('Ptak śpi');
31  }
32}
33
34// Latające ptaki
35class FlyingBird extends Bird implements Flyable {
36  makeSound(): void {
37    console.log('Ptak wydaje dźwięk');
38  }
39  
40  move(): void {
41    this.fly();
42  }
43  
44  fly(): void {
45    console.log('Ptak leci');
46  }
47}
48
49// Pływające ptaki
50class SwimmingBird extends Bird implements Swimmable {
51  makeSound(): void {
52    console.log('Ptak wydaje dźwięk');
53  }
54  
55  move(): void {
56    this.swim();
57  }
58  
59  swim(): void {
60    console.log('Ptak pływa');
61  }
62}
63
64// Konkretne implementacje
65class Sparrow extends FlyingBird {
66  makeSound(): void {
67    console.log('Ćwir ćwir!');
68  }
69  
70  fly(): void {
71    console.log('Wróbel leci szybko');
72  }
73}
74
75class Eagle extends FlyingBird {
76  makeSound(): void {
77    console.log('Krra!');
78  }
79  
80  fly(): void {
81    console.log('Orzeł szybuje majestatycznie');
82  }
83}
84
85class Penguin extends SwimmingBird implements Walkable {
86  makeSound(): void {
87    console.log('Kwa kwa!');
88  }
89  
90  swim(): void {
91    console.log('Pingwin pływa zwinnie');
92  }
93  
94  walk(): void {
95    console.log('Pingwin chodzi po lodzie');
96  }
97  
98  move(): void {
99    // Pingwin może się poruszać na różne sposoby
100    if (Math.random() > 0.5) {
101      this.swim();
102    } else {
103      this.walk();
104    }
105  }
106}
107
108// Funkcje przestrzegające LSP
109function makeAnimalMove(animal: Animal): void {
110  animal.move(); // Zawsze zadziała poprawnie
111}
112
113function makeFlyableObjectFly(flyable: Flyable): void {
114  flyable.fly(); // Tylko dla obiektów, które rzeczywiście potrafią latać
115}
116
117function makeSwimmableObjectSwim(swimmable: Swimmable): void {
118  swimmable.swim(); // Tylko dla obiektów, które rzeczywiście potrafią pływać
119}
120
121// Testowanie
122const birds: Bird[] = [
123  new Sparrow(),
124  new Eagle(),
125  new Penguin()
126];
127
128birds.forEach(bird => {
129  bird.makeSound();
130  makeAnimalMove(bird); // Każdy ptak może się poruszać po swojemu
131});
132
133// Testowanie zdolności latania tylko dla latających ptaków
134const flyingBirds: Flyable[] = [
135  new Sparrow(),
136  new Eagle()
137  // Pingwin nie jest dodany, bo nie implementuje Flyable
138];
139
140flyingBirds.forEach(bird => {
141  makeFlyableObjectFly(bird);
142});

I - Interface Segregation Principle (Zasada segregacji interfejsów)

Klasy nie powinny być zmuszone do implementowania interfejsów, z których nie korzystają.

Naruszenie ISP

1// ❌ Naruszenie ISP - zbyt duży interfejs
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: prosta drukarka musi implementować wszystkie metody
11class SimplePrinter implements MultiFunctionDevice {
12  print(document: string): void {
13    console.log(`Drukowanie: ${document}`);
14  }
15  
16  // Wymuszenie implementacji nieużywanych metod
17  scan(document: string): string {
18    throw new Error('Drukarka nie obsługuje skanowania');
19  }
20  
21  fax(document: string, number: string): void {
22    throw new Error('Drukarka nie obsługuje faksów');
23  }
24  
25  copy(document: string): string {
26    throw new Error('Drukarka nie obsługuje kopiowania');
27  }
28  
29  email(document: string, address: string): void {
30    throw new Error('Drukarka nie obsługuje emaili');
31  }
32}

Zgodne z ISP

1// ✅ Zgodne z ISP - podzielone interfejsy
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// Prosta drukarka implementuje tylko to, czego potrzebuje
24class SimplePrinter implements Printer {
25  print(document: string): void {
26    console.log(`Drukowanie: ${document}`);
27  }
28}
29
30// Skaner implementuje tylko skanowanie
31class SimpleScanner implements Scanner {
32  scan(document: string): string {
33    console.log(`Skanowanie: ${document}`);
34    return `Zeskanowany: ${document}`;
35  }
36}
37
38// Urządzenie wielofunkcyjne implementuje potrzebne interfejsy
39class MultiFunctionPrinter implements Printer, Scanner, Copier {
40  print(document: string): void {
41    console.log(`[MFP] Drukowanie: ${document}`);
42  }
43  
44  scan(document: string): string {
45    console.log(`[MFP] Skanowanie: ${document}`);
46    return `Zeskanowany: ${document}`;
47  }
48  
49  copy(document: string): string {
50    console.log(`[MFP] Kopiowanie: ${document}`);
51    const scanned = this.scan(document);
52    this.print(scanned);
53    return scanned;
54  }
55}
56
57// Profesjonalne urządzenie biurowe
58class OfficeMachine implements Printer, Scanner, FaxMachine, Copier, EmailSender {
59  print(document: string): void {
60    console.log(`[Office] Drukowanie: ${document}`);
61  }
62  
63  scan(document: string): string {
64    console.log(`[Office] Skanowanie: ${document}`);
65    return `Zeskanowany: ${document}`;
66  }
67  
68  fax(document: string, number: string): void {
69    console.log(`[Office] Wysyłanie faksu ${document} na numer ${number}`);
70  }
71  
72  copy(document: string): string {
73    console.log(`[Office] Kopiowanie: ${document}`);
74    return this.scan(document);
75  }
76  
77  email(document: string, address: string): void {
78    console.log(`[Office] Wysyłanie emaila ${document} na adres ${address}`);
79  }
80}
81
82// Funkcje używające tylko potrzebnych interfejsów
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// Użycie
98const simplePrinter = new SimplePrinter();
99const simpleScanner = new SimpleScanner();
100const mfp = new MultiFunctionPrinter();
101const officeMachine = new OfficeMachine();
102
103// Każde urządzenie używa tylko swoich możliwości
104DocumentProcessor.printDocument(simplePrinter, 'Dokument 1');
105DocumentProcessor.scanDocument(simpleScanner, 'Dokument 2');
106DocumentProcessor.copyDocument(mfp, 'Dokument 3');
107DocumentProcessor.printDocument(officeMachine, 'Dokument 4');

D - Dependency Inversion Principle (Zasada odwrócenia zależności)

Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji.

Naruszenie DIP

1// ❌ Naruszenie DIP - zależność od konkretnych implementacji
2
3class MySQLDatabase {
4  save(data: any): void {
5    console.log('Zapisywanie do MySQL:', data);
6  }
7  
8  find(id: string): any {
9    console.log('Szukanie w MySQL:', id);
10    return { id, data: 'mysql_data' };
11  }
12}
13
14class EmailNotification {
15  send(message: string, recipient: string): void {
16    console.log(`Wysyłanie emaila do ${recipient}: ${message}`);
17  }
18}
19
20// Problem: UserService zależy od konkretnych implementacji
21class UserService {
22  private database: MySQLDatabase; // Konkretna zależność
23  private notification: EmailNotification; // Konkretna zależność
24  
25  constructor() {
26    this.database = new MySQLDatabase(); // Tworzenie zależności wewnątrz klasy
27    this.notification = new EmailNotification();
28  }
29  
30  createUser(userData: any): void {
31    this.database.save(userData);
32    this.notification.send('Witaj!', userData.email);
33  }
34  
35  getUser(id: string): any {
36    return this.database.find(id);
37  }
38}

Zgodne z DIP

1// ✅ Zgodne z DIP - zależność od abstrakcji
2
3// Abstrakcje (interfejsy)
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// Konkretne implementacje - niski poziom
20class MySQLDatabase implements Database {
21  save(data: any): void {
22    console.log('Zapisywanie do MySQL:', data);
23  }
24  
25  find(id: string): any {
26    console.log('Szukanie w MySQL:', id);
27    return { id, data: 'mysql_data' };
28  }
29  
30  update(id: string, data: any): void {
31    console.log(`Aktualizacja w MySQL ${id}:`, data);
32  }
33  
34  delete(id: string): boolean {
35    console.log('Usuwanie z MySQL:', id);
36    return true;
37  }
38}
39
40class PostgreSQLDatabase implements Database {
41  save(data: any): void {
42    console.log('Zapisywanie do PostgreSQL:', data);
43  }
44  
45  find(id: string): any {
46    console.log('Szukanie w PostgreSQL:', id);
47    return { id, data: 'postgres_data' };
48  }
49  
50  update(id: string, data: any): void {
51    console.log(`Aktualizacja w PostgreSQL ${id}:`, data);
52  }
53  
54  delete(id: string): boolean {
55    console.log('Usuwanie z PostgreSQL:', id);
56    return true;
57  }
58}
59
60class MongoDatabase implements Database {
61  save(data: any): void {
62    console.log('Zapisywanie do MongoDB:', data);
63  }
64  
65  find(id: string): any {
66    console.log('Szukanie w MongoDB:', id);
67    return { id, data: 'mongo_data' };
68  }
69  
70  update(id: string, data: any): void {
71    console.log(`Aktualizacja w MongoDB ${id}:`, data);
72  }
73  
74  delete(id: string): boolean {
75    console.log('Usuwanie z MongoDB:', id);
76    return true;
77  }
78}
79
80class EmailNotificationService implements NotificationService {
81  async send(message: string, recipient: string): Promise<boolean> {
82    console.log(`Wysyłanie emaila do ${recipient}: ${message}`);
83    return true;
84  }
85}
86
87class SMSNotificationService implements NotificationService {
88  async send(message: string, recipient: string): Promise<boolean> {
89    console.log(`Wysyłanie SMS do ${recipient}: ${message}`);
90    return true;
91  }
92}
93
94class SlackNotificationService implements NotificationService {
95  async send(message: string, recipient: string): Promise<boolean> {
96    console.log(`Wysyłanie wiadomości Slack do ${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(`Zapisywanie do pliku [${level.toUpperCase()}]: ${message}`);
110  }
111}
112
113// Wysokopoziomowy moduł - zależy tylko od abstrakcji
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', `Tworzenie użytkownika: ${userData.email}`);
124      
125      this.database.save(userData);
126      
127      await this.notificationService.send(
128        'Witaj w naszej aplikacji!', 
129        userData.email
130      );
131      
132      this.logger.log('info', `Użytkownik ${userData.email} został utworzony`);
133    } catch (error) {
134      this.logger.log('error', `Błąd podczas tworzenia użytkownika: ${error}`);
135      throw error;
136    }
137  }
138  
139  getUser(id: string): any {
140    this.logger.log('info', `Pobieranie użytkownika: ${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', `Aktualizacja użytkownika: ${id}`);
147      
148      this.database.update(id, userData);
149      
150      await this.notificationService.send(
151        'Twoje dane zostały zaktualizowane', 
152        userData.email
153      );
154      
155      this.logger.log('info', `Użytkownik ${id} został zaktualizowany`);
156    } catch (error) {
157      this.logger.log('error', `Błąd podczas aktualizacji użytkownika: ${error}`);
158      throw error;
159    }
160  }
161  
162  deleteUser(id: string): boolean {
163    this.logger.log('info', `Usuwanie użytkownika: ${id}`);
164    return this.database.delete(id);
165  }
166}
167
168// Dependency Injection Container (uproszczony)
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// Konfiguracja zależności
186const container = new DIContainer();
187
188// Rejestracja różnych implementacji
189container.register<Database>('database', new PostgreSQLDatabase());
190container.register<NotificationService>('notification', new EmailNotificationService());
191container.register<Logger>('logger', new ConsoleLogger());
192
193// Utworzenie serwisu z wstrzykniętymi zależnościami
194const userService = new UserService(
195  container.get<Database>('database'),
196  container.get<NotificationService>('notification'),
197  container.get<Logger>('logger')
198);
199
200// Testowanie
201userService.createUser({
202  id: '1',
203  name: 'Jan Kowalski',
204  email: 'jan@example.com'
205});
206
207// Łatwe przełączanie implementacji
208const mobileUserService = new UserService(
209  new MongoDatabase(),              // Inna baza danych
210  new SMSNotificationService(),     // Inne powiadomienia
211  new FileLogger()                  // Inne logowanie
212);
213
214mobileUserService.createUser({
215  id: '2',
216  name: 'Anna Nowak',
217  email: '+48123456789'
218});

Praktyczny przykład - System e-commerce zgodny z SOLID

1// Kompleksowy przykład zastosowania wszystkich zasad SOLID
2
3// Abstrakcje
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 - łatwe dodawanie nowych strategii
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 - wszystkie strategie są zamienne
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 - małe, dedykowane interfejsy
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    // Sprawdzenie dostępności i obliczenie cen
134    for (const item of items) {
135      const product = await this.productRepository.findById(item.productId);
136      if (!product) {
137        throw new Error(`Produkt ${item.productId} nie został znaleziony`);
138      }
139      
140      const isAvailable = await this.inventoryService.checkAvailability(item.productId, item.quantity);
141      if (!isAvailable) {
142        throw new Error(`Niewystarczająca ilość produktu ${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    // Rezerwacja produktów
158    for (const item of orderItems) {
159      await this.inventoryService.reserveItems(item.productId, item.quantity);
160    }
161    
162    // Utworzenie zamówienia
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('Zamówienie nie zostało znalezione');
182    }
183    
184    order.status = 'confirmed';
185    await this.orderRepository.save(order);
186  }
187}
188
189// Przykład użycia z różnymi strategiami cenowymi
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% zniżki
213  
214  return new OrderService(
215    new InMemoryProductRepository(),
216    new InMemoryOrderRepository(),
217    new SimpleInventoryService(),
218    seasonalPricing,
219    new SlackNotificationService()
220  );
221}
222
223// Implementacje pomocnicze (uproszczone)
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: Potwierdzenie zamówienia ${order.id} wysłane`);
289  }
290  
291  async sendInventoryAlert(productId: string): Promise<void> {
292    console.log(`Email: Alert o niskim stanie magazynowym dla ${productId}`);
293  }
294}
295
296class SlackNotificationService implements NotificationService {
297  async sendOrderConfirmation(order: Order): Promise<void> {
298    console.log(`Slack: Potwierdzenie zamówienia ${order.id} wysłane`);
299  }
300  
301  async sendInventoryAlert(productId: string): Promise<void> {
302    console.log(`Slack: Alert o niskim stanie magazynowym dla ${productId}`);
303  }
304}

Podsumowanie zasad SOLID

Zasady SOLID to fundament dobrego projektowania oprogramowania:

  1. Single Responsibility - jedna klasa, jedna odpowiedzialność
  2. Open/Closed - otwarte na rozszerzenie, zamknięte na modyfikację
  3. Liskov Substitution - obiekty pochodne mogą zastąpić bazowe
  4. Interface Segregation - małe, dedykowane interfejsy
  5. Dependency Inversion - zależność od abstrakcji, nie implementacji

Stosowanie tych zasad w JavaScript/TypeScript prowadzi do kodu, który jest:

  • Łatwiejszy w utrzymaniu - zmiany w jednym miejscu nie wpływają na inne
  • Bardziej testowalny - możliwość mockowania zależności
  • Skalowalny - łatwe dodawanie nowych funkcjonalności
  • Czytelny - jasne role i odpowiedzialności

Podobnie jak starożytni budowniczowie przestrzegali zasad architektonicznych, współcześni programiści powinni kierować się zasadami SOLID, aby tworzyć aplikacje, które przetrwają próbę czasu.

Przejdź do CodeWorlds