Открыт для расширения, закрыт для изменения
Принцип открытости/закрытости гласит: код должен быть открыт для расширения — можно добавлять новое поведение, — но закрыт для изменения — существующий, работающий, протестированный код ради этого не правится.
Каждая правка работающего кода — это шанс его сломать. Когда добавление возможности означает только добавление нового кода, старый код остаётся проверенным.
Инструмент Python для этого — протокол: класс на основе typing.Protocol, который перечисляет методы, обязательные для типа. Любой класс с подходящими методами автоматически удовлетворяет протоколу — ничего объявлять не нужно, — поэтому функцию, зависящую от протокола, никогда не придётся менять, когда подключается новый класс.
match, который постоянно растёт
Этот калькулятор площади использует match по виду фигуры. Он работает — пока не появится следующая фигура. Треугольник? Правим match. Пятиугольник? Правим снова. Функция никогда не бывает законченной, и каждая правка подвергает риску фигуры, которые уже работали.
match по виду фигуры
total_area должна знать каждый вид фигуры. Добавление нового вида означает изменение этой протестированной функции — именно то, от чего предостерегает принцип.
Вывод: полная программа на Python
import math
from dataclasses import dataclass
@dataclass
class Shape:
kind: str
width: float = 0
height: float = 0
radius: float = 0
# total_area должна знать каждый вид — и растёт с каждым новым.
def total_area(shapes):
total = 0.0
for s in shapes:
match s.kind:
case "circle":
total += math.pi * s.radius * s.radius
case "rectangle":
total += s.width * s.height
return total
shapes = [
Shape(kind="circle", radius=2),
Shape(kind="rectangle", width=3, height=4),
]
print(f"общая площадь: {total_area(shapes):.2f}")
Расширение через протокол
Решение: каждая фигура сама знает свою площадь. Протокол Shape требует один метод, area(), а total_area просто складывает всё, что получает, — ей больше не важно, что это за фигуры.
Теперь посмотрите на принцип в действии: подключается совершенно новый Triangle, а total_area не затрагивается. Расширение без изменения.
Каждая фигура вычисляет свою площадь
total_area зависит только от протокола Shape. match исчез — а вместе с ним и необходимость когда-либо снова править эту функцию.
Вывод: полная программа на Python
import math
from dataclasses import dataclass
from typing import Protocol
class Shape(Protocol):
def area(self) -> float: ...
@dataclass
class Circle:
radius: float
def area(self) -> float:
return math.pi * self.radius * self.radius
@dataclass
class Rectangle:
width: float
height: float
def area(self) -> float:
return self.width * self.height
# total_area зависит только от протокола — нет match, нечего править.
def total_area(shapes: list[Shape]) -> float:
total = 0.0
for s in shapes:
total += s.area()
return total
shapes: list[Shape] = [
Circle(radius=2),
Rectangle(width=3, height=4),
]
print(f"общая площадь: {total_area(shapes):.2f}")
Добавляем новую фигуру, ничего не трогая
Triangle — полностью новый код: dataclass и его метод area(). total_area, Circle и Rectangle остались точно такими же, какими были, — открыты для расширения, закрыты для изменения.
Вывод: полная программа на Python
import math
from dataclasses import dataclass
from typing import Protocol
class Shape(Protocol):
def area(self) -> float: ...
@dataclass
class Circle:
radius: float
def area(self) -> float:
return math.pi * self.radius * self.radius
@dataclass
class Rectangle:
width: float
height: float
def area(self) -> float:
return self.width * self.height
# Triangle — НОВЫЙ код, всё, что выше, не тронуто.
@dataclass
class Triangle:
base: float
height: float
def area(self) -> float:
return self.base * self.height / 2
def total_area(shapes: list[Shape]) -> float:
total = 0.0
for s in shapes:
total += s.area()
return total
shapes: list[Shape] = [
Circle(radius=2),
Rectangle(width=3, height=4),
Triangle(base=6, height=5),
]
print(f"общая площадь: {total_area(shapes):.2f}")