Samy's blog

Today I Learned: SOLID - Single Responsibility Principle (SRP)

Today I Learned: SOLID - Single Responsibility Principle (SRP)

So lately I had this idea to kinda log about what I learned every day, with maybe a few examples for those who are interested to follow along, I hope this series serves others well too, anyway enough rambling.

First: What is SOLID? SOLID is an acronym for the first five object-oriented design (OOD) principles by Robert C. Martin (also known as Uncle Bob).

These principles establish practices that lend to developing software with considerations for maintaining and extending as the project grows. SOLID stands for:

  • S - Single-responsiblity Principle
  • O - Open-closed Principle
  • L - Liskov Substitution Principle
  • I - Interface Segregation Principle
  • D - Dependency Inversion Principle

Today I'm going to write about the first one, the ones that start with an S, its a basic concept really, as Uncle Bob states: "A class should have one, and only one, reason to change.",

In other words, a type should have one primary responsibility and as a result, it should have one reason to change. That reason being somehow related to its primary responsibility so let's take a look at how we can both adhere to the Single Responsibility Principle as well as how we can break it.

Lets say, we want to create the classic TODO list:

package main
import (
	"fmt"
	"io/ioutil"
	"net/url"
	"strings"
)

var entryCount = 0
type TODOList struct {
	entries []string
}

func (tdl  *TODOList) String() string {
	return strings.Join(tdl.entries, "\n")
}

func (tdl *TODOList) AddEntry(text string) int {
	entryCount++
	entry := fmt.Sprintf("%d: %s",
		entryCount,
		text)
	tdl.entries = append(tdl.entries, entry)
	return entryCount
}
func (tdl *TODOList) RemoveEntry(index int) {
	// ...
}

so far so good, but the SRP can easily be broken, by adding functions which for example deal with another concern (or problem), and if our little TODOList is trying to solve more than one problem, then..it has more than one responsibility, oh noes!.

This breaks SRP!

func (tdl *TODOList) Save(filename string) {
	_ = ioutil.WriteFile(filename,
		[]byte(tdl.String()), 0644)
}

func (tdl *TODOList) Load(filename string) {

}

func (tdl *TODOList) LoadFromWeb(url *url.URL) {

}


We need to separate the concerns of the struct, that struct is doing all kinds of things, managing lists, writing data to the disk 😥!

So to do so, what we can do to remove some burden from the poor struct is delegating some of the file writing stuff to another struct!

var lineSeparator = "\n"

type Persistence struct {
	lineSeparator string
}

func (p *Persistence) saveToFile(tdl *TODOList, filename string) {
	_ = ioutil.WriteFile(filename,
		[]byte(strings.Join(tdl.entries, p.lineSeparator)), 0644)
}


Now we have 2 structs, each one with its own concerns to attend, here is the full code:

package main
import (
	"fmt"
	"io/ioutil"
	"net/url"
	"strings"
)

var entryCount = 0
type TODOList struct {
	entries []string
}

func (tdl  *TODOList) String() string {
	return strings.Join(tdl.entries, "\n")
}

func (tdl *TODOList) AddEntry(text string) int {
	entryCount++
	entry := fmt.Sprintf("%d: %s",
		entryCount,
		text)
	tdl.entries = append(tdl.entries, entry)
	return entryCount
}
func (tdl *TODOList) RemoveEntry(index int) {
	// ...
}

/*
This breaks SRP!

func (tdl *TODOList) Save(filename string) {
	_ = ioutil.WriteFile(filename,
		[]byte(tdl.String()), 0644)
}

func (tdl *TODOList) Load(filename string) {

}

func (tdl *TODOList) LoadFromWeb(url *url.URL) {

}
*/

var lineSeparator = "\n"
func SaveToFile(tdl *TODOList, filename string) {
	_ = ioutil.WriteFile(filename,
		[]byte(strings.Join(tdl.entries, lineSeparator)), 0644)
}

type Persistence struct {
	lineSeparator string
}

func (p *Persistence) saveToFile(tdl *TODOList, filename string) {
	_ = ioutil.WriteFile(filename,
		[]byte(strings.Join(tdl.entries, p.lineSeparator)), 0644)
}

func main() {
	tdl := TODOList{}
	tdl.AddEntry("Learn something new.")
	tdl.AddEntry("Make blog post")
	fmt.Println(strings.Join(tdl.entries, "\n"))

	// separate function
	SaveToFile(&tdl, "journal.txt")

	//
	p := Persistence{"\n"}
	p.saveToFile(&tdl, "journal.txt")
}



Now our little TODOList is just focusing on what concerns it, dealing with its own list, and we delegated the file system stuff to the new Persistence struct, which has its own sets of concerns, like managing the line separator for the file written.

Separating these concerns into different entities allows us to extend the capabilities of each without worrying about breaking changes that might affect one another, if everything were to be on a single colossal struct, any changes to the struct or the functionality of its members might have affected other functionality inside itself...and...that's not good..and not fun.

Anyway!, that's pretty much it for this little piece..and as always:

A bit of tinkering a day keeps the boredom away. 🚀

#SOLID #Single-responsiblity Principle

- 6 toasts