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.
Każda klasa lub moduł powinien mieć tylko jeden powód do zmiany - czyli jedną, jasno określoną odpowiedzialność.
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}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});Klasy powinny być otwarte na rozszerzenie, ale zamknięte na modyfikację.
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();Obiekty klas pochodnych powinny móc zastąpić obiekty klasy bazowej bez wpływu na poprawność programu.
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}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});Klasy nie powinny być zmuszone do implementowania interfejsów, z których nie korzystają.
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}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');Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji.
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}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});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}Zasady SOLID to fundament dobrego projektowania oprogramowania:
Stosowanie tych zasad w JavaScript/TypeScript prowadzi do kodu, który jest:
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.