Что значит буква O в SOLID?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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;
}
}
Проблемы:
- Каждый раз, когда добавляется новый тип клиента, нужно менять
DiscountCalculator - Класс никогда не будет закончен (постоянно будут добавляться новые типы)
- Риск сломать существующую логику (и для
silver, и дляgold) - Нельзя использовать повторно разные стратегии скидок
Сценарий, который всегда происходит:
Вторник 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) — не усложняй без надобности
- Если это усложняет код для одного-двух случаев
Практический Совет
Правильный подход:
- Сначала напиши простой код (с if/else)
- Когда появляется второй вариант — рефакторь в интерфейс
- Когда появляется третий — точно нужна абстракция
// День 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'у
- Думай о том, что может измениться в будущем