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(),
}
}
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
}
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
}
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() {
}
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()
}
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.
Top comments (0)