SOLID

I created a simple program in Go, TypeScript, and PHP that implements all principles of SOLID.

To understand all SOLID principles, I suggest you read a book called Clean Architecture: A Craftsman's Guide to Software Structure and Design by Robert Cecil Martin, colloquially called Uncle Bob.

Clean Architecture

The SOLID principles tell us how to arrange our functions and data structures into classes, and how those classes should be interconnected. The use of the word “class” does not imply that these principles are applicable only to object-oriented software. A class is simply a coupled grouping of functions and data. Every software system has such groupings, whether they are called classes or not. The SOLID principles apply to those groupings.

The goal of the principles is the creation of mid-level software structures that:

I put the quotes from the book here. I don't want to change any theory. I think those quotes are clear enough.

What I want to show in this article is implementations. You can find implementations from other articles too. Most of them made an example for every principle. I also do the same, including a program that I made that applies all principles.

Shapes

Shapes Class Diagram

A simple app of GoLang to calculate the area and perimeter of shapes that implement SOLID. I also made another version in TypeScript and PHP. Feel free to clone the repositories:

Single Responsibility

S stands for Single Responsibility. The first principle of SOLID. One function should have one responsibility. Let's review the code below.

1
2
3
4
5
6
7
func (g *Guest) AddShape(s shape.Shape) {
    if !strings.Contains(fmt.Sprintf("%T", s), "*equilateral.") {
        return
    }

    g.Repo.AddShape(g, s)
}

It looks simple. But exactly, it has two responsibilities; checking permission and adding shape to the user. We can put it to another function and make it more dynamic.

Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func HasPermission(u User, s shape.Shape) bool {
    if fmt.Sprintf("%T", u) != "*user.Guest" {
        return true
    }
    if strings.Contains(fmt.Sprintf("%T", s), "*equilateral.") {
        return true
    }
    return false
}

func (g *Guest) AddShape(s shape.Shape) {
    if !HasPermission(g, s) {
        return
    }

    g.Repo.AddShape(g, s)
}

TypeScript

PHP

Open-Closed

A software artifact should be open for extension but closed for modification.

Take a look at our program, Shapes. All of components such as Shape, User, and Repository are open for extension but closed for modification. Thanks to interface.

Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Shape
    radius float64
}

func (c *Circle) Area() float64 {
    return math.Pi * math.Pow(c.radius, 2)
}

func (c *Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type Square struct {
    Shape
    side float64
}

func (s *Square) Area() float64 {
    return math.Pow(s.side, 2)
}

func (s *Square) Perimeter() float64 {
    return 4 * s.side
}

TypeScript

PHP

Liskov Substitution

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

That quote is from Barbara Liskov in 1988. You must be confused at the first time. That's why I made this article to bring an example of the implementation. Let's refactor our Shape to be more extendable using Liskov principle.

Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Equilateral interface {
    Shape
    SetA(float64)
}

type NonEquilateral interface {
    Equilateral
    SetB(float64)
}

type Circle struct {
    Equilateral
    radius float64
}

func (c *Circle) SetA(radius float64) {
    c.radius = radius
}

func (c *Circle) Area() float64 {
    return math.Pi * math.Pow(c.radius, 2)
}

func (c *Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

type Square struct {
    Equilateral
    side float64
}

func (s *Square) SetA(side float64) {
    s.side = side
}

func (s *Square) Area() float64 {
    return math.Pow(s.side, 2)
}

func (s *Square) Perimeter() float64 {
    return 4 * s.side
}

type Rectangle struct {
    NonEquilateral
    length, width float64
}

func (r *Rectangle) SetA(length float64) {
    r.length = length
}

func (r *Rectangle) SetB(width float64) {
    r.width = width
}

func (r *Rectangle) Area() float64 {
    return r.length * r.width
}

func (r *Rectangle) Perimeter() float64 {
    return 2 * (r.length + r.width)
}

TypeScript

PHP

As we can see from examples above, setA(), setB(), area(), and perimeter() have their own details or implementation. Circle and Square only need setA(), but a for Circle is radius and for Square is side. NonEquilateral has setB() that Rectangle needs. Circle and Square don't need setB(). It will break the Liskov principle if they are forced to implement setB().

Interface Segregation

The lesson here is that depending on something that carries baggage that you don't need can cause you troubles that you didn't expect.

Objects only have methods that they need.

Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type User interface {
    ID() uint32
    AddShape(shape.Shape)
    ShapeAreas() float64
    AverageShapeArea() float64
}

type PremiumUser interface {
    user.User
    ShapePerimeters() float64
    AverageShapePerimeter() float64
}

TypeScript

PHP

In our program, Shape, to get information of ShapePerimeters() and AverageShapePerimeter() are only for PremiumUser. If we only have one interface and even ShapePerimeters() and AverageShapePerimeter() return 0, it will break the Interface principle.

Dependency Inversion

The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.

The code below is a common example of the Dependency Inversion Principle.

Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type Repository interface {
    AddShape(User, shape.Shape)
    Shapes(User) []shape.Shape
    ShapeAreas(User) float64
    ShapePerimeters(User) float64
}

type Memory struct {
    shapes          map[uint32][]shape.Shape
    shapeAreas      map[uint32]float64
    shapePerimeters map[uint32]float64
}

func (m *Memory) AddShape(u user.User, s shape.Shape) {
    m.shapes[u.ID()] = append(m.shapes[u.ID()], s)
    m.shapeAreas[u.ID()] += s.Area()
    m.shapePerimeters[u.ID()] += s.Perimeter()
}

func (m *Memory) Shapes(u user.User) []shape.Shape {
    return m.shapes[u.ID()]
}

func (m *Memory) ShapeAreas(u user.User) float64 {
    return m.shapeAreas[u.ID()]
}

func (m *Memory) ShapePerimeters(u user.User) float64 {
    return m.shapePerimeters[u.ID()]
}

func NewMemory() Repository {
    return &Memory{
        shapes:          make(map[uint32][]shape.Shape),
        shapeAreas:      make(map[uint32]float64),
        shapePerimeters: make(map[uint32]float64),
    }
}

TypeScript

PHP

Repository is also an Anti-Corruption Layer (ACL). On our code above, the Repository only has the Memory implementation. No matter how many its implementation at the future, they need to implement all methods from the Repository. Those implementation will be consistent depend on the Repository. If we call the implementation directly, it will break the Dependency Inversion Principle.

Closing

I hope this single article can make you understand about SOLID principles. SOLID is important. It is a must even since the developers begin their journeys. It is one of foundations to make good software.

All tabs of examples in this article are powered by SemanticTabs. It is a VanillaJS library written in TypeScript, super lightweight, only 1.09 kB, to make semantic tabs.

References

I want to thanks to Marko Milojevic and DigitalOcean that inspire me to write this article. But I still suggest you to read the write an original paper of SOLID from Uncle Bob that introduced these software design patterns.

Related Articles

Comments