Открыт для расширения, закрыт для изменения
Принцип открытости/закрытости гласит: код должен быть открыт для расширения — новое поведение можно добавлять — но закрыт для изменения — существующий, работающий и проверенный код ради этого не правят.
Каждая правка работающего кода — это шанс его сломать. Когда добавление новой возможности означает только добавление нового кода, старый код остаётся проверенным.
Инструмент Go для этого — интерфейс: функцию, которая зависит от интерфейса, не нужно менять, когда его реализует новый тип.
switch, который никогда не перестаёт расти
Этот калькулятор площади использует switch по виду фигуры. Он работает — пока не появится следующая фигура. Треугольник? Правим switch. Пятиугольник? Правим снова. Функция никогда не бывает готова, и каждая правка ставит под угрозу фигуры, которые уже работали.
switch по виду фигуры
totalArea обязана знать каждый вид фигуры. Добавить один — значит изменить эту проверенную функцию, то есть сделать ровно то, от чего предостерегает принцип.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"math"
)
type shape struct {
kind string
width, height float64
radius float64
}
// totalArea обязана знать каждый вид фигуры — и растёт с каждым новым.
func totalArea(shapes []shape) float64 {
total := 0.0
for _, s := range shapes {
switch s.kind {
case "circle":
total += math.Pi * s.radius * s.radius
case "rectangle":
total += s.width * s.height
}
}
return total
}
func main() {
shapes := []shape{
{kind: "circle", radius: 2},
{kind: "rectangle", width: 3, height: 4},
}
fmt.Printf("общая площадь: %.2f\n", totalArea(shapes))
}
Расширение через интерфейс
Решение: каждая фигура сама знает свою площадь. Интерфейс Shape требует один метод, Area(), а totalArea просто складывает всё, что получает, — ей больше не важно, какие это фигуры.
Теперь посмотрим на принцип в действии: совершенно новый тип Triangle подключается, а totalArea остаётся нетронутой. Расширение без изменения.
Каждая фигура вычисляет свою площадь сама
totalArea зависит только от интерфейса Shape. switch исчез — а вместе с ним и необходимость когда-либо снова править эту функцию.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// totalArea зависит только от интерфейса — никакого switch, нечего править.
func totalArea(shapes []Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
func main() {
shapes := []Shape{
Circle{Radius: 2},
Rectangle{Width: 3, Height: 4},
}
fmt.Printf("общая площадь: %.2f\n", totalArea(shapes))
}
Добавляем новую фигуру, ничего не трогая
Triangle — это чистый новый код: тип и его метод Area(). totalArea, Circle и Rectangle остались ровно такими, какими были, — открыты для расширения, закрыты для изменения.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Triangle — это НОВЫЙ код, всё, что выше, осталось нетронутым.
type Triangle struct {
Base, Height float64
}
func (t Triangle) Area() float64 {
return t.Base * t.Height / 2
}
func totalArea(shapes []Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
func main() {
shapes := []Shape{
Circle{Radius: 2},
Rectangle{Width: 3, Height: 4},
Triangle{Base: 6, Height: 5},
}
fmt.Printf("общая площадь: %.2f\n", totalArea(shapes))
}