← Назад к вопросам

Что значит буква O в SOLID?

1.0 Junior🔥 151 комментариев
#Архитектура и паттерны#ООП

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Open/Closed Principle (OCP) — Буква O в SOLID

Буква O в SOLID означает Open/Closed Principle (принцип открытости/закрытости). Это один из самых мощных и одновременно самых неправильно применяемых принципов архитектуры.

Определение

Open/Closed Principle гласит:

Классы (или модули) должны быть открыты для расширения и закрыты для модификации.

Другими словами:

  • Ты должен иметь возможность добавлять новую функциональность БЕЗ изменения существующего кода
  • Нельзя менять старый код, нужно его расширять

Проблема без OCP

Пример: Система расчёта скидок

// ❌ Плохо — нарушает OCP
class DiscountCalculator {
  calculateDiscount(customerType: string, amount: number): number {
    if (customerType === 'regular') {
      return amount * 0.0; // Нет скидки
    } else if (customerType === 'silver') {
      return amount * 0.1; // 10% скидка
    } else if (customerType === 'gold') {
      return amount * 0.2; // 20% скидка
    } else if (customerType === 'platinum') {
      return amount * 0.3; // 30% скидка
    }
    return 0;
  }
}

Проблемы:

  1. Каждый раз, когда добавляется новый тип клиента, нужно менять DiscountCalculator
  2. Класс никогда не будет закончен (постоянно будут добавляться новые типы)
  3. Риск сломать существующую логику (и для silver, и для gold)
  4. Нельзя использовать повторно разные стратегии скидок

Сценарий, который всегда происходит:

Вторник 10:00 — добавляем тип 'platinum'
Среду 14:00 — уже нужен 'vip'
Четверг 9:00 — нужна скидка по промо-коду
Пятница — всё ломается, потому что разработчик неправильно изменил if/else

Решение: Используем Strategy Pattern

Правильный подход:

// 1. Интерфейс стратегии (открыт для расширения)
export interface DiscountStrategy {
  calculate(amount: number): number;
}

// 2. Конкретные стратегии (закрыты для модификации)
export class RegularCustomerDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return 0; // Нет скидки
  }
}

export class SilverCustomerDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.1; // 10%
  }
}

export class GoldCustomerDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.2; // 20%
  }
}

export class PlatinumCustomerDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.3; // 30%
  }
}

// 3. Калькулятор (закрыт для модификации)
export class DiscountCalculator {
  private strategy: DiscountStrategy;
  
  constructor(strategy: DiscountStrategy) {
    this.strategy = strategy;
  }
  
  calculateDiscount(amount: number): number {
    return this.strategy.calculate(amount);
  }
}

Использование:

// Каждый клиент имеет свою стратегию
const regularCustomer = new DiscountCalculator(new RegularCustomerDiscount());
const silverCustomer = new DiscountCalculator(new SilverCustomerDiscount());
const goldCustomer = new DiscountCalculator(new GoldCustomerDiscount());

console.log(silverCustomer.calculateDiscount(100)); // 10
console.log(goldCustomer.calculateDiscount(100));  // 20

Теперь добавляем новый тип клиента БЕЗ изменения существующего кода:

// Просто создаём новый класс
export class VIPCustomerDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.4; // 40%
  }
}

// DiscountCalculator не изменился! Он закрыт для модификации
const vipCustomer = new DiscountCalculator(new VIPCustomerDiscount());
console.log(vipCustomer.calculateDiscount(100)); // 40

OCP в Архитектуре: Плагины

Практический пример: Система обработки платежей

// Интерфейс платёжного провайдера (открыт для расширения)
export interface PaymentProvider {
  process(amount: number, currency: string): Promise<PaymentResult>;
  refund(transactionId: string): Promise<void>;
}

// Конкретные реализации (закрыты для модификации)
export class StripeProvider implements PaymentProvider {
  async process(amount: number, currency: string): Promise<PaymentResult> {
    // Реализация Stripe
  }
  async refund(transactionId: string): Promise<void> {
    // Refund через Stripe API
  }
}

export class PayPalProvider implements PaymentProvider {
  async process(amount: number, currency: string): Promise<PaymentResult> {
    // Реализация PayPal
  }
  async refund(transactionId: string): Promise<void> {
    // Refund через PayPal API
  }
}

export class ApplePayProvider implements PaymentProvider {
  async process(amount: number, currency: string): Promise<PaymentResult> {
    // Реализация Apple Pay
  }
  async refund(transactionId: string): Promise<void> {
    // Refund через Apple
  }
}

// Сервис платежей (закрыт для модификации)
export class PaymentService {
  constructor(private provider: PaymentProvider) {}
  
  async processPayment(amount: number, currency: string): Promise<PaymentResult> {
    return await this.provider.process(amount, currency);
  }
  
  async refundPayment(transactionId: string): Promise<void> {
    return await this.provider.refund(transactionId);
  }
}

Использование:

// В зависимости от конфига используем разного провайдера
const provider = process.env.PAYMENT_PROVIDER === 'stripe'
  ? new StripeProvider()
  : new PayPalProvider();

const paymentService = new PaymentService(provider);

// Один и тот же код работает с любым провайдером!
await paymentService.processPayment(100, 'USD');

Добавляем новый провайдер БЕЗ изменения PaymentService:

// Просто создаём новый класс
export class GooglePayProvider implements PaymentProvider {
  async process(amount: number, currency: string): Promise<PaymentResult> {
    // Google Pay реализация
  }
  async refund(transactionId: string): Promise<void> {
    // Refund
  }
}

// PaymentService не изменился!
const googlePay = new GooglePayProvider();
const paymentService = new PaymentService(googlePay);

OCP в Express Middleware

Практический пример: Система авторизации

// Интерфейс стратегии авторизации
interface AuthStrategy {
  authenticate(req: Request): Promise<User | null>;
}

// JWT стратегия
class JWTAuthStrategy implements AuthStrategy {
  async authenticate(req: Request): Promise<User | null> {
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) return null;
    return await this.verifyJWT(token);
  }
}

// OAuth2 стратегия
class OAuth2Strategy implements AuthStrategy {
  async authenticate(req: Request): Promise<User | null> {
    // OAuth2 логика
  }
}

// API Key стратегия
class APIKeyStrategy implements AuthStrategy {
  async authenticate(req: Request): Promise<User | null> {
    const apiKey = req.headers['x-api-key'];
    if (!apiKey) return null;
    return await this.findUserByApiKey(apiKey as string);
  }
}

// Middleware (закрыт для модификации)
function createAuthMiddleware(strategy: AuthStrategy) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const user = await strategy.authenticate(req);
    if (!user) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    req.user = user;
    next();
  };
}

// Использование
const authMiddleware = createAuthMiddleware(new JWTAuthStrategy());
app.use('/api/protected', authMiddleware);

OCP и Наследование

Когда не нужно использовать OCP:

// ❌ Переус лож но
class Animal {}
class Dog extends Animal { bark() {} }
class Cat extends Animal { meow() {} }

// Если меняется логика Animal, нужно менять Dog и Cat

// ✅ Лучше
interface Animal {
  makeSound(): void;
}
class Dog implements Animal {
  makeSound() { console.log('Woof'); }
}
class Cat implements Animal {
  makeSound() { console.log('Meow'); }
}

Правило Большого Пальца

Когда применять OCP:

  • Если ты предвидишь несколько вариантов реализации
  • Если код вероятно будет изменяться (новые платёжные системы, новые форматы вывода и т.д.)
  • Если ты пишешь библиотеку или фреймворк

Когда НЕ применять OCP:

  • Если нет очевидной потребности в расширении
  • Если YAGNI (You Aren't Gonna Need It) — не усложняй без надобности
  • Если это усложняет код для одного-двух случаев

Практический Совет

Правильный подход:

  1. Сначала напиши простой код (с if/else)
  2. Когда появляется второй вариант — рефакторь в интерфейс
  3. Когда появляется третий — точно нужна абстракция
// День 1: Простой код
if (type === 'stripe') {
  // stripe логика
}

// День 3: Появился PayPal
// Теперь рефакторим в интерфейс
interface PaymentProvider {
  process(): Promise<Result>;
}

// День 10: Появился Apple Pay
// Интерфейс уже готов, просто добавляем класс
class ApplePayProvider implements PaymentProvider { }

Заключение

Open/Closed Principle говорит:

  • Открыт для расширения: новые классы, новые реализации интерфейсов
  • Закрыт для модификации: не меняем старый код, добавляем новый

Это основа масштабируемой архитектуры. Система, которая следует OCP, может расти и развиваться БЕЗ риска сломать существующую функциональность.

Для backend разработчика это значит:

  • Пиши абстракции (интерфейсы) для вещей, которые могут меняться
  • Используй dependency injection
  • Предпочитай composition inheritance'у
  • Думай о том, что может измениться в будущем
Что значит буква O в SOLID? | PrepBro