Solid Principles in GO with examples

las - Aug 24 - - Dev Community

While Go isn't traditionally considered an object-oriented programming (OOP) language due to its lack of classes and objects, we can still apply SOLID principles to write cleaner, more maintainable code. Let's dive into how we can implement these principles in Go, demonstrating that good design transcends language paradigms.

What are SOLID Principles?

SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. Originally conceived for OOP, we'll see how they can be adapted to Go's unique features.

1. Single Responsibility Principle (SRP)

SRP states that a module should be responsible for one, and only one, reason to change. In Go, we can achieve this by creating focused structs and interfaces.

package singleresponsibility

import "time"

// SRP states that a module should be responsible for one, and only one, reason to change.
// In Go, we can achieve this by creating focused structs and interfaces.

// Creating the entity for order
type Order struct {
    OrderId    int
    OrderTotal float64
    CreatedAt  time.Time
}

// Defining the interface
type IOrderStore interface {
    CreateOrder() Order
}

// Defining the store and its dependencies
type OrderStore struct {
    // define the dependencies
}

func NewOrderStore() IOrderStore {
    return &OrderStore{}
}

func (*OrderStore) CreateOrder() Order {
    return Order{
        OrderId:    1,
        OrderTotal: 23.99,
        CreatedAt:  time.Now(),
    }
}
Enter fullscreen mode Exit fullscreen mode

By separating the Order entity from the OrderStore, we've ensured each struct has a single responsibility.

2. Open/Closed Principle (OCP)

OCP suggests that software entities should be open for extension but closed for modification. Go's interfaces make this principle easy to implement.

package openclosed

// OCP suggests that software entities should be open for extension but closed for modification.
// Go's interfaces make this principle easy to implement.

type HorsePowerCalculator interface {
    CalculateHorsePower() float64
}

type M3 struct{}

func NewM3() HorsePowerCalculator {
    return &M3{}
}

func (*M3) CalculateHorsePower() float64 {
    return 473
}

type Porche911 struct{}

func NewPorche911() HorsePowerCalculator {
    return &Porche911{}
}

func (*Porche911) CalculateHorsePower() float64 {
    return 388
}
Enter fullscreen mode Exit fullscreen mode

Here, we can add new car types without modifying existing code, simply by implementing the HorsePowerCalculator interface.

3. Liskov Substitution Principle (LSP)

LSP states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In Go, we can demonstrate this with interfaces.

package liskov

// LSP states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
// In Go, we can demonstrate this with interfaces.

type ICar interface {
    Drive() bool
}

type IFastCar interface {
    ICar
    Fast() bool
}

type M3 struct{}

type Altima struct{}

func NewM3() IFastCar {
    return &M3{}
}

func (*M3) Drive() bool {
    return true
}

func (*M3) Fast() bool {
    return true
}

func NewAltima() ICar {
    return &Altima{}
}

func (*Altima) Drive() bool {
    return true
}
Enter fullscreen mode Exit fullscreen mode

Both M3 and Altima can be used wherever an ICar is expected, with M3 providing additional functionality.

4. Interface Segregation Principle (ISP)

ISP advocates for many client-specific interfaces rather than one general-purpose interface. Go's lightweight interfaces are perfect for this.

package interfacesegregation

// ISP advocates for many client-specific interfaces rather than one general-purpose interface.
// Go's lightweight interfaces are perfect for this.

type User struct {
    Username string
}

type IReadUser interface {
    GetUser() User
}

type IWriteUser interface {
    CreateUser()
}

type UserReadStore struct{}

func NewUserRead() IReadUser {
    return &UserReadStore{}
}

func (*UserReadStore) GetUser() User {
    return User{
        Username: "pyro",
    }
}

type UserWriteStore struct{}

func NewUserWrite() IWriteUser {
    return &UserWriteStore{}
}

func (*UserWriteStore) CreateUser() {

}
Enter fullscreen mode Exit fullscreen mode

By separating read and write operations, clients can depend only on the methods they need.

5. Dependency Inversion Principle (DIP)

DIP states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

package dependencyinversion

// DIP states that high-level modules should not depend on low-level modules; both should depend on abstractions.
// Abstractions should not depend on details; details should depend on abstractions.

type User struct {
    Username string
}

type IReadUser interface {
    GetUser() User
}

type UserReadStore struct{}

func NewUserRead() IReadUser {
    return &UserReadStore{}
}

func (*UserReadStore) GetUser() User {
    return User{
        Username: "pyro",
    }
}

func (*UserReadStore) CreateUser() User {
    return User{
        Username: "pyro",
    }
}

func UserHandler() {

    userStore := NewUserRead()

    userStore.GetUser()

    // this function doesn't work because there is no abstraction implementation of it
    // userStore.CreateUser()
}

Enter fullscreen mode Exit fullscreen mode

UserHandler depends on the IReadUser interface, not on the concrete UserReadStore, allowing for easy substitution and testing.

Dive Into the Code

The complete code for this project is available on GitHub.

(https://github.com/pyrolass/grpc-microservice-go)

. .
Terabox Video Player