Open for Extension, Closed for Modification
The Open/Closed Principle says: code should be open for extension — new behavior can be added — but closed for modification — existing, working, tested code is not edited for it.
Every edit to working code is a chance to break it. When adding a feature means only adding new code, the old code stays proven.
Go's tool for this is the interface: a function that depends on an interface never needs to change when a new type implements it.
The switch That Never Stops Growing
This area calculator uses a switch on the shape kind. It works — until the next shape arrives. Triangle? Edit the switch. Pentagon? Edit it again. The function is never finished, and every edit risks the shapes that already worked.
A switch on the shape kind
totalArea must know every kind of shape. Adding one means modifying this tested function — the exact thing the principle warns against.
Output: Go program and captured output
package main
import (
"fmt"
"math"
)
type shape struct {
kind string
width, height float64
radius float64
}
// totalArea must know every kind — and grows with each new one.
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("total area: %.2f\n", totalArea(shapes))
}
Extend Through an Interface
The fix: each shape knows its own area. The Shape interface asks for one method, Area(), and totalArea just adds up whatever it receives — it no longer cares what the shapes are.
Now watch the principle at work: a brand-new Triangle plugs in, and totalArea is not touched. Extension without modification.
Each shape computes its own area
totalArea depends only on the Shape interface. The switch is gone — and with it, the need to ever edit this function again.
Output: Go program and captured output
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 depends only on the interface — no switch, nothing to edit.
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("total area: %.2f\n", totalArea(shapes))
}
Add a new shape, touch nothing
Triangle is pure new code: a type and its Area() method. totalArea, Circle and Rectangle are exactly as they were — open for extension, closed for modification.
Output: Go program and captured output
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 is NEW code — everything above is untouched.
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("total area: %.2f\n", totalArea(shapes))
}