12.1.2 — вторая буква SOLID12.1.2 — the second letter of SOLID

Open/Closed Principle (OCP)

Программные сущности должны быть открыты для расширения, но закрыты для модификации. Новое поведение добавляется новым кодом рядом со старым, а не правкой уже протестированного и работающего кода.Software entities should be open for extension but closed for modification. New behavior gets added via new code alongside the old, not by editing code that already works and is already tested.

частый вопрос на собеседованииcommon interview question SOLID · 2/5

Что этоWhat it is

Сформулирован Бертраном Мейером (1988), позже переосмыслен Робертом Мартином через полиморфизм и абстракции вместо наследования реализации. Суть: если добавление новой разновидности поведения требует захода в существующий, уже работающий класс и добавления туда if/switch по типу — принцип нарушен. Правильная реализация выносит точку расширения за интерфейс/абстрактный класс, а новая разновидность добавляется отдельным классом, реализующим этот интерфейс.Formulated by Bertrand Meyer (1988), later reframed by Robert Martin around polymorphism and abstraction rather than implementation inheritance. The core idea: if adding a new kind of behavior requires reaching into an existing, already-working class and adding an if/switch on type, the principle is violated. A correct implementation moves the extension point behind an interface/abstract class, and a new variant is added as a separate class implementing that interface.

До и послеBefore and after

нарушение — switch по типу растёт с каждой фигуройviolation — a type switch grows with every new shape
double area(const Shape& s) {
    switch (s.type) {
        case ShapeType::Circle:    return M_PI * s.radius * s.radius;
        case ShapeType::Rectangle: return s.w * s.h;
        // новая фигура — правим эту функцию и рискуем сломать старые ветки// a new shape — edit this function and risk breaking the existing branches
    }
}
расширение через полиморфизм — старый код не трогаемextension via polymorphism — the old code stays untouched
struct Shape { virtual double area() const = 0; };
struct Circle : Shape {
    double area() const override { return M_PI * r * r; }
};
struct Rectangle : Shape {
    double area() const override { return w * h; }
};
// новая фигура — новый класс, ни одна существующая строка не меняется// a new shape — a new class, not a single existing line changes
struct Triangle : Shape { double area() const override; };

OCP без наследованияOCP without inheritance

01

Rust trait objects — тот же принцип, без иерархии классовRust trait objects — the same principle, no class hierarchy

Rust не имеет наследования реализации — расширение точки поведения делается через trait плюс новую реализацию, что структурно и есть OCP, только без слова «класс».Rust has no implementation inheritance — extending a behavior point is done via a trait plus a new implementation, which structurally is OCP, just without the word "class."

trait Shape { fn area(&self) -> f64; }

struct Circle { r: f64 }
impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
}
// новый impl Shape for Triangle — существующий код не трогаем// a new impl Shape for Triangle — the existing code stays untouched
02

Strategy pattern как воплощение OCPThe Strategy pattern as an embodiment of OCP

Паттерн Strategy — прямая реализация OCP: алгоритм выносится за интерфейс, новая стратегия добавляется отдельным классом. Это одна из причин, почему OCP на практике почти всегда реализуется через уже известные паттерны GoF, а не изобретается заново.The Strategy pattern is a direct implementation of OCP: the algorithm is moved behind an interface, and a new strategy is added as a separate class. This is part of why OCP in practice is almost always implemented through already-known GoF patterns rather than reinvented from scratch.

ГраблиGotchas

Абстракция «на будущее» без реальной второй реализацииSpeculative abstraction with no real second implementation

Введение интерфейса ради гипотетического будущего расширения, которое так и не наступает — это чистая цена (лишний уровень косвенности, сложнее читать) без выгоды. OCP оправдан, когда точки расширения реально возникают на практике, а не предугадываются заранее «про запас».Introducing an interface for a hypothetical future extension that never actually materializes is pure cost (an extra layer of indirection, harder to read) with no payoff. OCP earns its keep when extension points actually arise in practice, not when they're speculatively guessed at in advance "just in case."

Полное соблюдение OCP невозможно для всех осей изменения сразуFull OCP compliance across every axis of change at once is impossible

Мартин прямо отмечал: нельзя сделать модуль закрытым сразу для всех видов изменений — разработчик должен выбрать, к каким конкретно изменениям система будет наиболее устойчива, основываясь на опыте и наблюдаемых паттернах реальных правок, а не пытаться закрыться от всего сразу.Martin himself noted: you cannot make a module closed to every kind of change simultaneously — the developer has to choose which specific changes the system will be most resilient to, based on experience and observed patterns of actual edits, rather than trying to close off against everything at once.

Виртуальная диспетчеризация — не бесплатнаDynamic dispatch isn't free

Классическая реализация OCP через наследование и virtual-вызовы платит косвенным вызовом по vtable и потерей возможности инлайнить — в горячих путях высокопроизводительного кода это может быть неприемлемо, и OCP там достигается статическим полиморфизмом (шаблоны/generic), а не runtime-диспетчеризацией.The classic OCP implementation via inheritance and virtual calls pays for an indirect vtable call and a lost inlining opportunity — in hot paths of performance-critical code this can be unacceptable, and OCP there is achieved through static polymorphism (templates/generics) rather than runtime dispatch.