The SOLID principles are a set of guidelines for writing maintainable and scalable software. They were introduced by Robert C. Martin in the late 1990s and have since become an integral part of software engineering best practices. The SOLID principles are relevant today as they provide a roadmap for writing clean and maintainable code, which is especially important in today’s rapidly changing software landscape. In this blog, we’ll be discussing the SOLID principles and how to apply SOLID Principals in Golang, one of the fastest-growing programming languages.
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should only have one responsibility. In Golang, this can be achieved by creating small, focused functions that perform specific tasks.
But first, let us understand this by an example that breaks the principle and backtrack to the solution.
package main
import (
"fmt"
"math"
)
type circle struct {
radius float64
}
func (c circle) area() {
// violating Single Responsibility Principle
fmt.Printf("circle area: %f\n", math.Pi*c.radius*c.radius)
}
func main() {
c := circle{
radius: 3,
}
c.area()
}
This example breaks SRP
because the function area
will have to be changed for two reasons, the formula of area changes, and second the output of the program changes.
Let us now write a code, that follows SRP and solves the problem.
package main
import (
"fmt"
"math"
)
type circle struct {
radius float64
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) print() {
fmt.Printf("The are of circle is %f \n", c.area())
}
type square struct {
length float64
}
func (c square) print() {
fmt.Printf("The are of square is %f \n", c.area())
}
func (c square) area() float64 {
return c.length * c.length
}
type shape interface {
name() string
area() float64
}
func main() {
c := circle{
radius: 3,
}
c.print()
s := square{
length: 3,
}
s.print()
}
In this example, you only have to change the function area
when the formula changes and the function print
when the printing format changes.
2. Open/Closed Principle (OCP)
The Open/Closed Principle states that a class should be open for extension but closed for modification. In other words, the behavior of a class should be able to be extended without modifying its source code. In Golang, this can be achieved by using interfaces and polymorphism.
package main
import "fmt"
// This is an interface that defines a shape.
type Shape interface {
Area() float64
}
// This is a struct that implements the Shape interface.
type Rectangle struct {
width float64
height float64
}
// This method calculates the area of a rectangle.
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// This is a struct that implements the Shape interface.
type Circle struct {
radius float64
}
// This method calculates the area of a circle.
func (c Circle) Area() float64 {
return 3.1415926535 * c.radius * c.radius
}
// This function takes a Shape as an argument and calculates its area.
func calculateArea(s Shape) float64 {
return s.Area()
}
func main() {
rect := Rectangle{width: 10, height: 5}
fmt.Println("Area of rectangle:", calculateArea(rect))
circle := Circle{radius: 5}
fmt.Println("Area of circle:", calculateArea(circle))
}
As seen in this example, all the structs are open for extension but not for modification.
3. Liskov Substitution Principle
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, a derived class should be able to substitute its base class without affecting the functionality of the program.
Here’s an example of how the Liskov Substitution Principle can be applied in Golang:
package main
import "fmt"
// This is an interface that defines a shape.
type Shape interface {
Area() float64
}
// This is a struct that implements the Shape interface.
type Rectangle struct {
width float64
height float64
}
// This method calculates the area of a rectangle.
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// This is a struct that implements the Shape interface.
type Square struct {
side float64
}
// This method calculates the area of a square.
func (s Square) Area() float64 {
return s.side * s.side
}
// This function takes a Shape as an argument and calculates its area.
func calculateArea(s Shape) float64 {
return s.Area()
}
func main() {
rect := Rectangle{width: 10, height: 5}
fmt.Println("Area of rectangle:", calculateArea(rect))
square := Square{side: 5}
fmt.Println("Area of square:", calculateArea(square))
}
In this example, the Square
struct implements the Shape
interface and thus can be used wherever a Shape
is expected. This allows us to substitute a Square
object for a Rectangle
object without affecting the correctness of the program, as both have a Area()
method that implements the Shape
interface. This is an example of the Liskov Substitution Principle in action.
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use. In Golang, this can be achieved by creating smaller, focused interfaces that provide only the functionality needed by a specific client.
package main
import "fmt"
// This is an interface that defines basic operations for a shape.
type BasicShape interface {
Area() float64
Perimeter() float64
}
// This is an interface that defines advanced operations for a shape.
type AdvancedShape interface {
Volume() float64
}
// This is a struct that implements the BasicShape interface.
type Rectangle struct {
width float64
height float64
}
// This method calculates the area of a rectangle.
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// This method calculates the perimeter of a rectangle.
func (r Rectangle) Perimeter() float64 {
return 2 * (r.width + r.height)
}
// This is a struct that implements the BasicShape and AdvancedShape interfaces.
type Cube struct {
side float64
}
// This method calculates the area of a cube.
func (c Cube) Area() float64 {
return 6 * c.side * c.side
}
// This method calculates the perimeter of a cube.
func (c Cube) Perimeter() float64 {
return 12 * c.side
}
// This method calculates the volume of a cube.
func (c Cube) Volume() float64 {
return c.side * c.side * c.side
}
// This function takes a BasicShape as an argument and calculates its area.
func calculateArea(s BasicShape) float64 {
return s.Area()
}
// This function takes an AdvancedShape as an argument and calculates its volume.
func calculateVolume(s AdvancedShape) float64 {
return s.Volume()
}
func main() {
rect := Rectangle{width: 10, height: 5}
fmt.Println("Area of rectangle:", calculateArea(rect))
cube := Cube{side: 5}
fmt.Println("Area of cube:", calculateArea(cube))
fmt.Println("Volume of cube:", calculateVolume(cube))
}
In this example, the BasicShape
interface defines the basic operations (area and perimeter) for a shape, while the AdvancedShape
interface defines advanced operations (volume) for a shape. The Rectangle
struct implements only the BasicShape
interface, while the Cube
struct implements both the BasicShape
and AdvancedShape
interfaces. This allows us to use the Rectangle
struct with the calculateArea()
function, and the Cube
struct with both the calculateArea()
and calculateVolume()
functions, without forcing the clients to depend on methods they don’t use.
5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules, but both should depend on abstractions.
Here’s an example of how the Dependency Inversion Principle can be applied in Golang:
package main
import "fmt"
// This is an interface that defines basic operations for a database.
type Database interface {
Connect()
Store(data string)
}
// This is a struct that implements the Database interface.
type MySQL struct {}
// This method connects to a MySQL database.
func (m MySQL) Connect() {
fmt.Println("Connecting to MySQL database...")
}
// This method stores data in a MySQL database.
func (m MySQL) Store(data string) {
fmt.Println("Storing data in MySQL database:", data)
}
// This is a struct that implements the Database interface.
type PostgreSQL struct {}
// This method connects to a PostgreSQL database.
func (p PostgreSQL) Connect() {
fmt.Println("Connecting to PostgreSQL database...")
}
// This method stores data in a PostgreSQL database.
func (p PostgreSQL) Store(data string) {
fmt.Println("Storing data in PostgreSQL database:", data)
}
// This is a struct that depends on a Database interface.
type Service struct {
db Database
}
// This method sets the Database for the Service.
func (s *Service) SetDatabase(db Database) {
s.db = db
}
// This method stores data in a database using the Database interface.
func (s *Service) StoreData(data string) {
s.db.Connect()
s.db.Store(data)
}
func main() {
// This creates a Service that uses a MySQL database.
mysqlService := Service{db: MySQL{}}
mysqlService.StoreData("Hello, world!")
// This creates a Service that uses a PostgreSQL database.
postgresService := Service{db: PostgreSQL{}}
postgresService.StoreData("Hello, world!")
}
In this example, the Service
struct depends on the Database
interface, and the MySQL
and PostgreSQL
structs implement the Database
interface. This means that the Service
struct can store data in either a MySQL
or PostgreSQL
database, without having to know or care which database it’s using. This is an example of the Dependency Inversion Principle in action. The high-level Service
module depends on an abstraction (the Database
interface), while both the low-level MySQL
and PostgreSQL
modules depend on the same abstraction. This allows the Service
module to be decoupled from the specific implementation of the database, making it more flexible and maintainable.
The SOLID principles in Golang are a set of guidelines for writing maintainable and scalable software. They provide a roadmap for writing clean and maintainable code, which is especially important in today’s rapidly changing software landscape. Using Golang, these principles can be easily applied to create well-designed, flexible, and scalable applications. By following the SOLID principles, developers can write software that is easier to maintain, less prone to bugs, and more scalable in the long run.
We have more articles on Design Patterns and other Golang. Feel free to go through them.
0 Comments