Программные сущности должны быть открыты для расширения, но закрыты для изменения.
Поведение должно быть можно расширить – например, через наследование или композицию, – но не нужно лезть в существующий код и переписывать его ради новых сценариев.
Почему это важно
Работающий код уже проверен: он прошёл тесты, ревью и продакшен. Каждая правка в нём – это риск сломать то, что работало. Добавление нового класса такого риска не несёт.
Как выглядит нарушение
Тип клиента проверяется в трёх разных местах:
// Новый тип клиента – правка сразу трёх работающих методов
public decimal Discount(string customerType, decimal amount)
{
if (customerType == "premium") return amount * 0.1m;
return 0m;
}
public int OrderLimit(string customerType) => customerType == "premium" ? 100 : 10;
public string WelcomeMail(string customerType) =>
customerType == "premium" ? "Спасибо, что вы с нами!" : "Добро пожаловать!";
// Новый тип клиента – правка сразу трёх работающих функций
double Discount(const std::string& customer_type, double amount) {
return customer_type == "premium" ? amount * 0.1 : 0;
}
int OrderLimit(const std::string& customer_type) {
return customer_type == "premium" ? 100 : 10;
}
std::string WelcomeMail(const std::string& customer_type) {
return customer_type == "premium" ? "Спасибо, что вы с нами!" : "Добро пожаловать!";
}
// Новый тип клиента – правка сразу трёх работающих функций
func Discount(customerType string, amount float64) float64 {
if customerType == "premium" {
return amount * 0.1
}
return 0
}
func OrderLimit(customerType string) int {
if customerType == "premium" {
return 100
}
return 10
}
func WelcomeMail(customerType string) string {
if customerType == "premium" {
return "Спасибо, что вы с нами!"
}
return "Добро пожаловать!"
}
# Новый тип клиента – правка сразу трёх работающих функций
def discount(customer_type: str, amount: float) -> float:
return amount * 0.1 if customer_type == "premium" else 0
def order_limit(customer_type: str) -> int:
return 100 if customer_type == "premium" else 10
def welcome_mail(customer_type: str) -> str:
return "Спасибо, что вы с нами!" if customer_type == "premium" else "Добро пожаловать!"
// Новый тип клиента – правка сразу трёх работающих функций
function discount(customerType: string, amount: number): number {
return customerType === 'premium' ? amount * 0.1 : 0;
}
function orderLimit(customerType: string): number {
return customerType === 'premium' ? 100 : 10;
}
function welcomeMail(customerType: string): string {
return customerType === 'premium' ? 'Спасибо, что вы с нами!' : 'Добро пожаловать!';
}
// Новый тип клиента – правка сразу трёх работающих методов
public BigDecimal discount(String customerType, BigDecimal amount) {
return customerType.equals("premium") ? amount.multiply(new BigDecimal("0.1")) : BigDecimal.ZERO;
}
public int orderLimit(String customerType) {
return customerType.equals("premium") ? 100 : 10;
}
public String welcomeMail(String customerType) {
return customerType.equals("premium") ? "Спасибо, что вы с нами!" : "Добро пожаловать!";
}
// Новый тип клиента – правка сразу трёх работающих функций
fun discount(customerType: String, amount: BigDecimal): BigDecimal =
if (customerType == "premium") amount * BigDecimal("0.1") else BigDecimal.ZERO
fun orderLimit(customerType: String) = if (customerType == "premium") 100 else 10
fun welcomeMail(customerType: String) =
if (customerType == "premium") "Спасибо, что вы с нами!" else "Добро пожаловать!"
Приходит отдел продаж: нужен третий тип клиента – партнёр, со своей скидкой, своим лимитом и своим письмом. Что с этим не так:
- правим то, что работает – три проверенных метода придётся открыть и дописать, а вместе с ними переписать их тесты;
- компилятор молчит – он не знает, что проверок типа три; забыли одну – партнёр молча получит лимит обычного клиента, и выяснится это в проде;
- ветки размножаются – через год типов семь, и каждый живёт в семи разных
if; - две команды в одном файле – кто добавляет партнёра, а кто корпоративного клиента, встречаются в конфликте слияния.
Как открыть для расширения
Ось изменений здесь видна: меняется тип клиента. Значит, его и делаем точкой расширения – одним интерфейсом на все три решения:
// Новый тип клиента – новый класс, старые не трогаем
public interface ICustomerPolicy
{
decimal Discount(decimal amount);
int OrderLimit { get; }
string WelcomeMail();
}
public class RegularCustomer : ICustomerPolicy
{
public decimal Discount(decimal amount) => 0m;
public int OrderLimit => 10;
public string WelcomeMail() => "Добро пожаловать!";
}
public class PremiumCustomer : ICustomerPolicy
{
public decimal Discount(decimal amount) => amount * 0.1m;
public int OrderLimit => 100;
public string WelcomeMail() => "Спасибо, что вы с нами!";
}
// Партнёр приезжает отдельным файлом – ни одна строка выше не меняется
public class PartnerCustomer : ICustomerPolicy
{
public decimal Discount(decimal amount) => amount * 0.25m;
public int OrderLimit => 1000;
public string WelcomeMail() => "Рады работать вместе!";
}
// Новый тип клиента – новый класс, старые не трогаем
class ICustomerPolicy {
public:
virtual double Discount(double amount) const = 0;
virtual int OrderLimit() const = 0;
virtual std::string WelcomeMail() const = 0;
virtual ~ICustomerPolicy() = default;
};
class RegularCustomer : public ICustomerPolicy {
public:
double Discount(double) const override { return 0; }
int OrderLimit() const override { return 10; }
std::string WelcomeMail() const override { return "Добро пожаловать!"; }
};
class PremiumCustomer : public ICustomerPolicy {
public:
double Discount(double amount) const override { return amount * 0.1; }
int OrderLimit() const override { return 100; }
std::string WelcomeMail() const override { return "Спасибо, что вы с нами!"; }
};
// Партнёр приезжает отдельным файлом – ни одна строка выше не меняется
class PartnerCustomer : public ICustomerPolicy {
public:
double Discount(double amount) const override { return amount * 0.25; }
int OrderLimit() const override { return 1000; }
std::string WelcomeMail() const override { return "Рады работать вместе!"; }
};
// Новый тип клиента – новая реализация интерфейса, старые не трогаем
type CustomerPolicy interface {
Discount(amount float64) float64
OrderLimit() int
WelcomeMail() string
}
type RegularCustomer struct{}
func (RegularCustomer) Discount(float64) float64 { return 0 }
func (RegularCustomer) OrderLimit() int { return 10 }
func (RegularCustomer) WelcomeMail() string { return "Добро пожаловать!" }
type PremiumCustomer struct{}
func (PremiumCustomer) Discount(amount float64) float64 { return amount * 0.1 }
func (PremiumCustomer) OrderLimit() int { return 100 }
func (PremiumCustomer) WelcomeMail() string { return "Спасибо, что вы с нами!" }
// Партнёр приезжает отдельным файлом – ни одна строка выше не меняется
type PartnerCustomer struct{}
func (PartnerCustomer) Discount(amount float64) float64 { return amount * 0.25 }
func (PartnerCustomer) OrderLimit() int { return 1000 }
func (PartnerCustomer) WelcomeMail() string { return "Рады работать вместе!" }
# Новый тип клиента – новый класс, старые не трогаем
class CustomerPolicy(Protocol):
def discount(self, amount: float) -> float: ...
def order_limit(self) -> int: ...
def welcome_mail(self) -> str: ...
class RegularCustomer:
def discount(self, amount: float) -> float:
return 0
def order_limit(self) -> int:
return 10
def welcome_mail(self) -> str:
return "Добро пожаловать!"
class PremiumCustomer:
def discount(self, amount: float) -> float:
return amount * 0.1
def order_limit(self) -> int:
return 100
def welcome_mail(self) -> str:
return "Спасибо, что вы с нами!"
# Партнёр приезжает отдельным файлом – ни одна строка выше не меняется
class PartnerCustomer:
def discount(self, amount: float) -> float:
return amount * 0.25
def order_limit(self) -> int:
return 1000
def welcome_mail(self) -> str:
return "Рады работать вместе!"
// Новый тип клиента – новый класс, старые не трогаем
interface CustomerPolicy {
discount(amount: number): number;
orderLimit(): number;
welcomeMail(): string;
}
class RegularCustomer implements CustomerPolicy {
discount(): number {
return 0;
}
orderLimit(): number {
return 10;
}
welcomeMail(): string {
return 'Добро пожаловать!';
}
}
class PremiumCustomer implements CustomerPolicy {
discount(amount: number): number {
return amount * 0.1;
}
orderLimit(): number {
return 100;
}
welcomeMail(): string {
return 'Спасибо, что вы с нами!';
}
}
// Партнёр приезжает отдельным файлом – ни одна строка выше не меняется
class PartnerCustomer implements CustomerPolicy {
discount(amount: number): number {
return amount * 0.25;
}
orderLimit(): number {
return 1000;
}
welcomeMail(): string {
return 'Рады работать вместе!';
}
}
// Новый тип клиента – новый класс, старые не трогаем
public interface CustomerPolicy {
BigDecimal discount(BigDecimal amount);
int orderLimit();
String welcomeMail();
}
public class RegularCustomer implements CustomerPolicy {
public BigDecimal discount(BigDecimal amount) { return BigDecimal.ZERO; }
public int orderLimit() { return 10; }
public String welcomeMail() { return "Добро пожаловать!"; }
}
public class PremiumCustomer implements CustomerPolicy {
public BigDecimal discount(BigDecimal amount) { return amount.multiply(new BigDecimal("0.1")); }
public int orderLimit() { return 100; }
public String welcomeMail() { return "Спасибо, что вы с нами!"; }
}
// Партнёр приезжает отдельным файлом – ни одна строка выше не меняется
public class PartnerCustomer implements CustomerPolicy {
public BigDecimal discount(BigDecimal amount) { return amount.multiply(new BigDecimal("0.25")); }
public int orderLimit() { return 1000; }
public String welcomeMail() { return "Рады работать вместе!"; }
}
// Новый тип клиента – новая реализация, старые не трогаем
interface CustomerPolicy {
fun discount(amount: BigDecimal): BigDecimal
val orderLimit: Int
fun welcomeMail(): String
}
class RegularCustomer : CustomerPolicy {
override fun discount(amount: BigDecimal): BigDecimal = BigDecimal.ZERO
override val orderLimit = 10
override fun welcomeMail() = "Добро пожаловать!"
}
class PremiumCustomer : CustomerPolicy {
override fun discount(amount: BigDecimal) = amount * BigDecimal("0.1")
override val orderLimit = 100
override fun welcomeMail() = "Спасибо, что вы с нами!"
}
// Партнёр приезжает отдельным файлом – ни одна строка выше не меняется
class PartnerCustomer : CustomerPolicy {
override fun discount(amount: BigDecimal) = amount * BigDecimal("0.25")
override val orderLimit = 1000
override fun welcomeMail() = "Рады работать вместе!"
}
Проверок типа больше нет: вызывающий код просит у политики скидку и лимит, не зная, чья она. Добавить партнёра – значит добавить файл, а не открыть три работающих.
Что даёт соблюдение
- Работающий код не трогают – значит, не ломают. Регрессия не может появиться в файле, который никто не редактировал.
- Новую функциональность можно делать параллельно. Две команды добавляют два новых типа скидки и не встречаются в одном файле.
- Откат стоит дёшево. Убрать новое поведение – это удалить класс, а не выискивать в диффе строки, которые дописали в общий метод.
- Прошлые проверки остаются в силе. Тесты старых типов клиентов не переписываются, и им можно доверять после релиза.
Где принцип превращается в проблему
Чтобы сущность была открыта для расширения, нужно заранее угадать ось, вдоль которой пойдут изменения. Угадали неверно – получили абстракцию, которая мешает, и всё равно правите существующий код. Поэтому OCP хорошо применять на второй-третьей похожей задаче, а не на первой.