SOLID is a famous cargo-cult that is used to poke “bad” code during code review. Jokes aside there are some solid, pun intended, ideas within SOLID. This post is yet another attempt to dismantle this set of principles and understand them better.

The SOLID stands for(pasted from wiki):

  • S ingle-responsibility principle: “There should never be more than one reason for a class to change.“In other words, every class should have only one responsibility
  • O pen–closed principle: “Software entities … should be open for extension, but closed for modification.”
  • L iskov substitution principle: “Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.“See also design by contract.
  • I nterface segregation principle: “Many client-specific interfaces are better than one general-purpose interface.”
  • D ependency inversion principle: “Depend upon abstractions, [not] concretions.”

S

single responsibility

Out of all these, only first one makes sense right away. If you are not a complete beginner, you won’t create a struct that does shipping calculation, checks weather condition, sends push notification and orders pizza. Don’t you?

Single responsibility principle also relates to package naming and content. Packages with e.g. name “common” become a dump that does many things and exported everywhere.

O

open for extension, closed for modification
package main

type A struct {
        year int
}

func (a A) Greet() { fmt.Println("Hello GolangUK", a.year) }

type B struct {
        A
}

func (b B) Greet() { fmt.Println("Welcome to GolangUK", b.year) }

func main() {
        var a A
        a.year = 2016
        var b B
        b.year = 2016
        a.Greet() // Hello GolangUK 2016
        b.Greet() // Welcome to GolangUK 2016
}

In this example by Dave Cheney, although

Thanks to embedding B can use, but not change non-exported fields from A. Embedding in go naturally allows golang types to be open for extension by overriding or adding new methods. At the same time there is no way you can change Greet() in A struct from B struct. Well, you can change year, of course.

L

Liskov substitution principle

“two types are substitutable if they behave in the way such that the caller is unable to tell difference between them”

I

interface segregation or do not depend on stuff you do not need

In Layman terms do not force your code to depened on stuff it does not need or use. Which basically means — keep your interfaces small, it will help to have interface clients to not depend on stuff they do not need.

I will just copy-paste here a brilliant example by Dave Cheney:

1 func Save(f *os.File, d *Document) error
                   │
                   ▼
2 func Save(f io.ReadWriteCloser, d *Document) error
                   │
                   ▼
3 func Save(f io.WriteCloser, d *Document) error
                   │
                   ▼
4 func Save(f io.Writer, d *Document) error 

Simply put, on line 1 Save method can only save to file, which does not allow us to “save” to any other place. It also makes testing harder — you have to use a real file for testing. Evolution of Save method boils down to the simple signature and at the same the most benevolent one. Now anything that implements io.Writer can be used to Save document.

D

dependency inversion

Depend on abstactions, not concretions. I think it means that everything you use in your structs should rely on interfaces and not concrete instances.

Sources: