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

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

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

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

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

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

Как выглядит нарушение

Тип клиента проверяется в трёх разных местах:

// Новый тип клиента – правка сразу трёх работающих методов
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 хорошо применять на второй-третьей похожей задаче, а не на первой.