Zasady SOLID w JavaScript

SOLID to zestaw pięciu zasad projektowania obiektowego, mających na celu tworzenie łatwo utrzymywalnego i rozszerzalnego kodu. Stosowanie tych zasad ułatwia organizację kodu, zwiększa jego czytelność i ułatwia współpracę w zespole. Poniżej znajdziesz omówienie każdej z nich oraz przykłady "błędne" i "poprawne" w JavaScripcie.

1. Single Responsibility Principle (SRP)

Zasada pojedynczej odpowiedzialności: Klasa lub moduł powinny mieć tylko jedną odpowiedzialność, czyli jeden powód do zmiany.

Przykład błędny

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  saveToDatabase() {
    // Logika zapisu do bazy danych
    console.log(`Saving ${this.name} to database`);
  }

  sendEmail() {
    // Logika wysyłania emaila
    console.log(`Sending email to ${this.email}`);
  }

  validateEmail() {
    // Logika walidacji emaila
    return this.email.includes('@');
  }
}

Przykład poprawny

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserRepository {
  saveUser(user) {
    // Logika zapisu do bazy danych
    console.log(`Saving ${user.name} to database`);
  }
}

class EmailService {
  sendEmail(user) {
    // Logika wysyłania emaila
    console.log(`Sending email to ${user.email}`);
  }
}

class EmailValidator {
  static validate(email) {
    return email.includes('@');
  }
}

2. Open/Closed Principle (OCP)

Zasada otwarte-zamknięte: Klasy powinny być otwarte na rozszerzanie, ale zamknięte na modyfikacje. Nową funkcjonalność należy dodawać poprzez rozszerzanie istniejącego kodu, nie modyfikując go bezpośrednio.

Przykład błędny

class PaymentProcessor {
  processPayment(payment) {
    if (payment.type === 'credit') {
      // Logika dla karty kredytowej
    } else if (payment.type === 'debit') {
      // Logika dla karty debetowej
    } else if (payment.type === 'paypal') {
      // Logika dla PayPal
    }
  }
}

Przykład poprawny

class PaymentProcessor {
  processPayment(paymentMethod) {
    return paymentMethod.process();
  }
}

class CreditCardPayment {
  process() {
    // Logika dla karty kredytowej
  }
}

class DebitCardPayment {
  process() {
    // Logika dla karty debetowej
  }
}

class PayPalPayment {
  process() {
    // Logika dla PayPal
  }
}

3. Liskov Substitution Principle (LSP)

Zasada podstawienia Liskov: Obiekty klas bazowych muszą być zastępowalne przez obiekty klas pochodnych bez wpływu na poprawność działania programu. Innymi słowy, jeśli S jest podtypem T, to obiekty typu T mogą być zastąpione obiektami typu S bez zmiany poprawności programu.

Przykład błędny

class Bird {
  fly() {
    return "I can fly";
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("I can't fly"); // Łamie LSP
  }
}

Przykład poprawny

class Bird {
  move() {
    return "I can move";
  }
}

class FlyingBird extends Bird {
  fly() {
    return "I can fly";
  }
}

class SwimmingBird extends Bird {
  swim() {
    return "I can swim";
  }
}

4. Interface Segregation Principle (ISP)

Zasada segregacji interfejsów: Lepiej jest mieć wiele małych, wyspecjalizowanych interfejsów niż jeden ogólny. Klient nie powinien być zmuszony do implementacji metod, których nie używa.

Przykład błędny

class Worker {
  work() {
    // Implementacja pracy
  }
  
  eat() {
    // Implementacja jedzenia
  }
  
  sleep() {
    // Implementacja spania
  }
}

class Robot extends Worker {
  eat() {
    throw new Error("Robots don't eat");
  }
  
  sleep() {
    throw new Error("Robots don't sleep");
  }
}

Przykład poprawny

class Workable {
  work() {
    // Implementacja pracy
  }
}

class NeedsRest {
  sleep() {
    // Implementacja spania
  }
}

class NeedsFood {
  eat() {
    // Implementacja jedzenia
  }
}

class Human extends Workable {
  constructor(foodNeeds, restNeeds) {
    super();
    this.foodNeeds = foodNeeds;
    this.restNeeds = restNeeds;
  }
}

class Robot extends Workable {
  // Tylko praca, bez jedzenia i spania
}

5. Dependency Inversion Principle (DIP)

Zasada odwrócenia zależności: Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Oba powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółów implementacji.

Przykład błędny

class EmailNotifier {
  constructor() {
    this.emailService = new EmailService();
  }

  notify(user, message) {
    this.emailService.sendEmail(user.email, message);
  }
}

Przykład poprawny

class Notifier {
  constructor(notificationService) {
    this.notificationService = notificationService;
  }

  notify(user, message) {
    this.notificationService.send(user, message);
  }
}

class EmailService {
  send(user, message) {
    // Wysyłanie emaila
  }
}

class SMSService {
  send(user, message) {
    // Wysyłanie SMS
  }
}

// Użycie:
const emailNotifier = new Notifier(new EmailService());
const smsNotifier = new Notifier(new SMSService());

Podsumowanie

Stosowanie zasad SOLID pomaga tworzyć kod, który jest:

Zasady SOLID są szczególnie istotne w JavaScript, gdzie:

Każda z zasad SOLID koncentruje się na innym aspekcie projektowania obiektowego, ale wszystkie razem tworzą spójny zestaw wytycznych prowadzących do lepszej jakości kodu.