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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export const HasPermission = (u: User, s: Shape): boolean => {
  if (u.constructor.name != 'Guest') {
    return true
  }

  switch (s.constructor.name) {
    case 'Circle':
    case 'Square':
      return true
  }

  return false
}

public addShape(s: Shape): void {
  if (!HasPermission(this, s)) {
    return
  }

  this._repo.addShape(this, s)
}

PHP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
public static function hasPermission(User $u, Shape $s): bool {
  if (!in_array('User\User', class_implements($u))) {
    return true;
  }
  if (in_array('Shape\Equilateral\Equilateral', class_implements($s))) {
    return true;
  }
  return false;
}

public function addShape(Shape $s): void {
  if (!Permission::hasPermission($this, $s)) {
    return;
  }

  $this->repo->addShape($this, $s);
}

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

 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
export interface Shape {
  area(): number
  perimeter(): number
}

export class Circle implements Shape {
  private _radius: number

  constructor(radius: number) {
    this._radius = radius
  }

  public area(): number {
    return Math.PI * Math.pow(this._radius, 2)
  }

  public perimeter(): number {
    return 2 * Math.PI * this._radius
  }
}

export class Square implements Shape {
  private _side: number

  constructor(side: number) {
    this._side = side
  }

  public area(): number {
    return Math.pow(this._side, 2)
  }

  public perimeter(): number {
    return 4 * this._side
  }
}

PHP

 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
<?php
interface Shape {
  public function area(): float;
  public function perimeter(): float;
}

class Circle implements Shape {
  private float $radius;

  function __construct(float $radius) {
    $this->radius = $radius;
  }

  public function area(): float {
    return pi() * pow($this->radius, 2);
  }

  public function perimeter(): float {
    return 2 * pi() * $this->radius;
  }
}

class Square implements Shape {
  private float $side;

  function __construct(float $side) {
    $this->side = $side;
  }

  public function area(): float {
    return pow($this->side, 2);
  }

  public function perimeter(): float {
    return 4 * $this->side;
  }
}

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

 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
70
71
72
73
74
75
76
77
78
export interface Shape {
  area(): number;
  perimeter(): number
}

export interface Equilateral extends Shape {
  set a(a: number)
}

export interface NonEquilateral extends Equilateral {
  set b(b: number)
}

export class Circle implements Equilateral {
  private _radius: number

  constructor() {
    this._radius = 0
  }

  set a(a: number) {
    this._radius = a
  }

  public area(): number {
    return Math.PI * Math.pow(this._radius, 2)
  }

  public perimeter(): number {
    return 2 * Math.PI * this._radius
  }
}

export class Square implements Equilateral {
  private _side: number

  constructor() {
    this._side = 0
  }

  set a(a: number) {
    this._side = a
  }

  public area(): number {
    return Math.pow(this._side, 2)
  }

  public perimeter(): number {
    return 4 * this._side
  }
}

export class Rectangle implements NonEquilateral {
  private _length: number
  private _width: number

  constructor() {
    this._length = 0
    this._width = 0
  }

  set a(a: number) {
    this._length = a
  }

  set b(b: number) {
    this._width = b
  }

  public area(): number {
    return this._length * this._width
  }

  public perimeter(): number {
    return 2 * (this._length + this._width)
  }
}

PHP

 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
<?php
interface Shape {
  public function area(): float;
  public function perimeter(): float;
}

interface Equilateral extends Shape {
  public function setA(float $a): void;
}

interface NonEquilateral extends Equilateral {
  public function setB(float $b): void;
}

class Circle implements Equilateral {
  private float $radius;

  public function setA(float $a): void {
    $this->radius = $a;
  }

  public function area(): float {
    return pi() * pow($this->radius, 2);
  }

  public function perimeter(): float {
    return 2 * pi() * $this->radius;
  }
}

class Square implements Equilateral {
  private float $side;

  public function setA(float $a): void {
    $this->side = $a;
  }

  public function area(): float {
    return pow($this->side, 2);
  }

  public function perimeter(): float {
    return 4 * $this->side;
  }
}

class Rectangle implements NonEquilateral {
  private float $length;
  private float $width;

  public function setA(float $a): void {
    $this->length = $a;
  }

  public function setB(float $b): void {
    $this->width = $b;
  }

  public function area(): float {
    return $this->length * $this->width;
  }

  public function perimeter(): float {
    return 2 * ($this->length + $this->width);
  }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export interface User {
  get id(): bigint
  addShape(shape: Shape): void
  get shapeAreas(): number
  get averageShapeArea(): number
}

export interface PremiumUser extends User {
  get shapePerimeters(): number
  get averageShapePerimeter(): number
}

PHP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
interface User {
  public function addShape(Shape $shape): void;
  public function shapeAreas(): float;
  public function averageShapeArea(): float;
}

interface PremiumUser extends User {
  public function shapePerimeters(): float;
  public function averageShapePerimeter(): float;
}

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

 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
export interface Repository {
  addShape(user: User, shape: Shape): void
  shapes(user: User): Shape[]
  shapeAreas(user: User): number
  shapePerimeters(user: User): number
}

export default class Memory implements Repository {
  private _shapes: Map<bigint, Shape[]>
  private _shapeAreas: Map<bigint, number>
  private _shapePerimeters: Map<bigint, number>

  constructor() {
    this._shapes = new Map<bigint, Shape[]>()
    this._shapeAreas = new Map<bigint, number>()
    this._shapePerimeters = new Map<bigint, number>()
  }

  public addShape(u: User, s: Shape): void {
    if ((<Shape[]>this._shapes.get(u.id)) === undefined) {
      this._shapes.set(u.id, new Array<Shape>())
    }
    (<Shape[]>this._shapes.get(u.id)).push(s)

    if (this._shapeAreas.get(u.id) === undefined) {
      this._shapeAreas.set(u.id, 0)
    }
    this._shapeAreas.set(u.id, Number(this._shapeAreas.get(u.id)) + s.area())

    if (this._shapePerimeters.get(u.id) === undefined) {
      this._shapePerimeters.set(u.id, 0)
    }
    this._shapePerimeters.set(u.id, Number(this._shapePerimeters.get(u.id)) + s.perimeter())
  }

  public shapes(u: User): Shape[] {
    return <Shape[]>this._shapes.get(u.id)
  }

  public shapeAreas(u: User): number {
    return Number(this._shapeAreas.get(u.id))
  }

  public shapePerimeters(u: User): number {
    return Number(this._shapePerimeters.get(u.id))
  }
}

PHP

 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
<?php
interface Repository {
  public function addShape(User $u, Shape $s): void;
  public function shapes(User $u): array;
  public function shapeAreas(User $u): float;
  public function shapePerimeters(User $u): float;
}

class Memory implements Repository {
  private array $shapes;
  private array $shapeAreas;
  private array $shapePerimeters;

  public function addShape(User $u, Shape $s): void {
    $this->shapes[$u->id][] = $s;
    if (empty($this->shapeAreas[$u->id])) {
      $this->shapeAreas[$u->id] = 0;
    }
    $this->shapeAreas[$u->id] += $s->area();
    if (empty($this->shapePerimeters[$u->id])) {
      $this->shapePerimeters[$u->id] = 0;
    }
    $this->shapePerimeters[$u->id] += $s->perimeter();
  }

  public function shapes(User $u): array {
    return $this->shapes[$u->id];
  }

  public function shapeAreas(User $u): float {
    return $this->shapeAreas[$u->id];
  }

  public function shapePerimeters(User $u): float {
    return $this->shapePerimeters[$u->id];
  }
}

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