SOLID principles

Single Responsibility Principle(S)

One class should have one, and only one, responsibility- In any well-designed system Objects should only a have a single responsibility - In a nutshell, object implementations should only focus on doing one thing well and in an efficient way - We should separate a big function into smaller functions - This is bad
func loadUserHandlerLong(resp http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}
userID, err := strconv.ParseInt(req.Form.Get("UserID"), 10, 64)
if err != nil {
resp.WriteHeader(http.StatusPreconditionFailed)
return
}
row := DB.QueryRow("SELECT * FROM Users WHERE ID = ?", userID)
person := &Person{}
err = row.Scan(&person.ID, &person.Name, &person.Phone)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}
encoder := json.NewEncoder(resp)
err = encoder.Encode(person)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}
}

We should do this

func loadUserHandlerSRP(resp http.ResponseWriter, req *http.Request) {
userID, err := extractIDFromRequest(req)
if err != nil {
resp.WriteHeader(http.StatusPreconditionFailed)
return
}
person, err := loadPersonByID(userID)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}
outputPerson(resp, person)
}
func extractIDFromRequest(req *http.Request) (int64, error) {
err := req.ParseForm()
if err != nil {
return 0, err
}
return strconv.ParseInt(req.Form.Get("UserID"), 10, 64)
}
func loadPersonByID(userID int64) (*Person, error) {
row := DB.QueryRow("SELECT * FROM Users WHERE userID = ?", userID)
person := &Person{}
err := row.Scan(person.ID, person.Name, person.Phone)
if err != nil {
return nil, err
}
return person, nil
}
func outputPerson(resp http.ResponseWriter, person *Person) {
encoder := json.NewEncoder(resp)
err := encoder.Encode(person)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}
}

Open/closed principle

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

  • Bad code
func BuildOutputOCPFail(response http.ResponseWriter, format string, person Person) {
var err error
switch format {
case "csv":
err = outputCSV(response, person)
case "json":
err = outputJSON(response, person)
}
if err != nil {
// output a server error and quit
response.WriteHeader(http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
}
// output the person as CSV and return error when failing to do so
func outputCSV(writer io.Writer, person Person) error {
// TODO: implement
return nil
}
// output the person as JSON and return error when failing to do so
func outputJSON(writer io.Writer, person Person) error {
// TODO: implement
return nil
}
// A data transfer object that represents a person
type Person struct {
Name string
Email string
}
  • Good code
func BuildOutputOCPSuccess(response http.ResponseWriter, formatter PersonFormatter, person Person) {
err := formatter.Format(response, person)
if err != nil {
// output a server error and quit
response.WriteHeader(http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
}
type PersonFormatter interface {
Format(writer io.Writer, person Person) error
}
// output the person as CSV
type CSVPersonFormatter struct{}
// Format implements the PersonFormatter interface
func (c *CSVPersonFormatter) Format(writer io.Writer, person Person) error {
// TODO: implement
return nil
}
// output the person as JSON
type JSONPersonFormatter struct{}
// Format implements the PersonFormatter interface
func (j *JSONPersonFormatter) Format(writer io.Writer, person Person) error {
// TODO: implement
return nil
}

Liskov substitution principle

If we have a parent class and a child class, then we can interchange the parent and child class without getting incorrect results. The child class must implement everything that's in the parent class.

Bad code

package lsp_violation
func Go(vehicle actions) {
if sled, ok := vehicle.(*Sled); ok {
sled.pushStart()
} else {
vehicle.startEngine()
}
vehicle.drive()
}
type actions interface {
drive()
startEngine()
}
type Vehicle struct {
}
func (v Vehicle) drive() {
// TODO: implement
}
func (v Vehicle) startEngine() {
// TODO: implement
}
func (v Vehicle) stopEngine() {
// TODO: implement
}
type Car struct {
Vehicle
}
type Sled struct {
Vehicle
}
func (s Sled) startEngine() {
// override so that is does nothing
}
func (s Sled) stopEngine() {
// override so that is does nothing
}
func (s Sled) pushStart() {
// TODO: implement
}

Good code

package fixedv2
func Go(vehicle actions) {
vehicle.start()
vehicle.drive()
}
type actions interface {
start()
drive()
}
type Car struct {
poweredVehicle
}
func (c Car) start() {
c.poweredVehicle.startEngine()
}
func (c Car) drive() {
// TODO: implement
}
type poweredVehicle struct {
}
func (p poweredVehicle) startEngine() {
// common engine start code
}
type Sled struct {
}
func (s Sled) start() {
// push start
}
func (s Sled) drive() {
// TODO: implement
}

Interface Segregation Principle

The interface segregation principle states that "clients shouldn't be forced to depend on interfaces that they don't use

  • Bad code
type FatDbInterface interface {
BatchGetItem(IDs ...int) ([]Item, error)
BatchGetItemWithContext(ctx context.Context, IDs ...int) ([]Item, error)
BatchPutItem(items ...Item) error
BatchPutItemWithContext(ctx context.Context, items ...Item) error
DeleteItem(ID int) error
DeleteItemWithContext(ctx context.Context, item Item) error
GetItem(ID int) (Item, error)
GetItemWithContext(ctx context.Context, ID int) (Item, error)
PutItem(item Item) error
PutItemWithContext(ctx context.Context, item Item) error
Query(query string, args ...interface{}) ([]Item, error)
QueryWithContext(ctx context.Context, query string, args ...interface{}) ([]Item, error)
UpdateItem(item Item) error
UpdateItemWithContext(ctx context.Context, item Item) error
}
  • Good code
type myDB interface {
GetItem(ID int) (Item, error)
PutItem(item Item) error
}

https://stackoverflow.com/a/57126786

Last updated on