This lesson is not translated yet – the Russian original is below.
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 хорошо применять на второй-третьей похожей задаче, а не на первой.