Программа курса← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF

OCP – принцип открытости и закрытости

TODO: отвалидировать – текст собран автоматически из материалов курса на Google Drive, проверьте формулировки и код.

Программные сущности должны быть открыты для расширения, но закрыты для изменения.

Поведение должно быть можно расширить – например, через наследование или композицию, – но не нужно лезть в существующий код и переписывать его ради новых сценариев.

Почему это важно

Работающий код уже проверен: он прошёл тесты, ревью и продакшен. Каждая правка в нём – это риск сломать то, что работало. Добавление нового класса такого риска не несёт.

// Закрыт для расширения: новый тип клиента – правка метода
public decimal GetDiscount(string customerType)
{
    if (customerType == "regular") return 0m;
    if (customerType == "premium") return 0.1m;

    return 0m;
}

// Открыт для расширения: новый тип клиента – новый класс
public interface IDiscountPolicy
{
    decimal GetDiscount(decimal amount);
}

public class RegularDiscount : IDiscountPolicy
{
    public decimal GetDiscount(decimal amount) => 0m;
}

public class PremiumDiscount : IDiscountPolicy
{
    public decimal GetDiscount(decimal amount) => amount * 0.1m;
}
// Закрыт для расширения: новый тип клиента – правка функции
double GetDiscount(const std::string& customer_type, double amount) {
    if (customer_type == "regular") return 0;
    if (customer_type == "premium") return amount * 0.1;

    return 0;
}

// Открыт для расширения: новый тип клиента – новый класс
class IDiscountPolicy {
public:
    virtual double GetDiscount(double amount) const = 0;
    virtual ~IDiscountPolicy() = default;
};

class RegularDiscount : public IDiscountPolicy {
public:
    double GetDiscount(double amount) const override { return 0; }
};

class PremiumDiscount : public IDiscountPolicy {
public:
    double GetDiscount(double amount) const override { return amount * 0.1; }
};
// Закрыт для расширения: новый тип клиента – правка функции
func GetDiscount(customerType string, amount float64) float64 {
	if customerType == "regular" {
		return 0
	}
	if customerType == "premium" {
		return amount * 0.1
	}

	return 0
}

// Открыт для расширения: новый тип клиента – новая реализация интерфейса
type DiscountPolicy interface {
	Discount(amount float64) float64
}

type RegularDiscount struct{}

func (RegularDiscount) Discount(amount float64) float64 { return 0 }

type PremiumDiscount struct{}

func (PremiumDiscount) Discount(amount float64) float64 { return amount * 0.1 }
# Закрыт для расширения: новый тип клиента – правка функции
def get_discount(customer_type: str, amount: float) -> float:
    if customer_type == "regular":
        return 0
    if customer_type == "premium":
        return amount * 0.1

    return 0

# Открыт для расширения: новый тип клиента – новый класс
from typing import Protocol

class DiscountPolicy(Protocol):
    def discount(self, amount: float) -> float: ...

class RegularDiscount:
    def discount(self, amount: float) -> float:
        return 0

class PremiumDiscount:
    def discount(self, amount: float) -> float:
        return amount * 0.1
// Закрыт для расширения: новый тип клиента – правка функции
function getDiscount(customerType: string, amount: number): number {
    if (customerType === "regular") return 0;
    if (customerType === "premium") return amount * 0.1;

    return 0;
}

// Открыт для расширения: новый тип клиента – новый класс
interface DiscountPolicy {
    discount(amount: number): number;
}

class RegularDiscount implements DiscountPolicy {
    discount(amount: number): number {
        return 0;
    }
}

class PremiumDiscount implements DiscountPolicy {
    discount(amount: number): number {
        return amount * 0.1;
    }
}
// Закрыт для расширения: новый тип клиента – правка метода
public BigDecimal getDiscount(String customerType, BigDecimal amount) {
    if (customerType.equals("regular")) return BigDecimal.ZERO;
    if (customerType.equals("premium")) return amount.multiply(new BigDecimal("0.1"));

    return BigDecimal.ZERO;
}

// Открыт для расширения: новый тип клиента – новый класс
public interface DiscountPolicy {
    BigDecimal discount(BigDecimal amount);
}

public class RegularDiscount implements DiscountPolicy {
    public BigDecimal discount(BigDecimal amount) {
        return BigDecimal.ZERO;
    }
}

public class PremiumDiscount implements DiscountPolicy {
    public BigDecimal discount(BigDecimal amount) {
        return amount.multiply(new BigDecimal("0.1"));
    }
}
// Закрыт для расширения: новый тип клиента – правка функции
fun getDiscount(customerType: String, amount: BigDecimal): BigDecimal = when (customerType) {
    "regular" -> BigDecimal.ZERO
    "premium" -> amount * BigDecimal("0.1")
    else -> BigDecimal.ZERO
}

// Открыт для расширения: новый тип клиента – новая реализация
fun interface DiscountPolicy {
    fun discount(amount: BigDecimal): BigDecimal
}

class RegularDiscount : DiscountPolicy {
    override fun discount(amount: BigDecimal): BigDecimal = BigDecimal.ZERO
}

class PremiumDiscount : DiscountPolicy {
    override fun discount(amount: BigDecimal): BigDecimal = amount * BigDecimal("0.1")
}

Что даёт соблюдение

  • Работающий код не трогают – значит, не ломают. Регрессия не может появиться в файле, который никто не редактировал.
  • Новую функциональность можно делать параллельно. Две команды добавляют два новых типа скидки и не встречаются в одном файле.
  • Откат стоит дёшево. Убрать новое поведение – это удалить класс, а не выискивать в диффе строки, которые дописали в общий метод.
  • Прошлые проверки остаются в силе. Тесты старых типов клиентов не переписываются, и им можно доверять после релиза.

Где принцип превращается в проблему

Чтобы сущность была открыта для расширения, нужно заранее угадать ось, вдоль которой пойдут изменения. Угадали неверно – получили абстракцию, которая мешает, и всё равно правите существующий код. Поэтому OCP хорошо применять на второй-третьей похожей задаче, а не на первой.