Understanding SOLID Principles with Golang

Author Profile Pic
Anurag
Published on Sat Aug 03 2024 ~ 8 min read
Understanding SOLID Principles with Golang

SOLID is an acronym for five principles of object-oriented design that can help developers create more flexible, understandable, and maintainable software. These principles were introduced by Robert C. Martin, also known as Uncle Bob. They are:


  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)


In this blog, we'll explore each principle with simple examples in Go (Golang).


Single Responsibility Principle (SRP)


The Single Responsibility Principle states that a class (or in Go, a struct) should have only one reason to change, meaning it should only have one job or responsibility.


Example in Go


Consider a simple application that processes orders. Initially, we might be tempted to create a single struct that handles everything:


package main

import "fmt"

type Order struct {
	ID    int
	Items []string
}

func (o *Order) CalculateTotal() float64 {
	total := 0.0
	for _, item := range o.Items {
		total += 10.0 // Simplified: Assume each item costs $10
	}
	return total
}

func (o *Order) PrintOrder() {
	fmt.Printf("Order #%d: %v\n", o.ID, o.Items)
}

func main() {
	order := &Order{ID: 1, Items: []string{"item1", "item2"}}
	total := order.CalculateTotal()
	order.PrintOrder()
	fmt.Printf("Total: $%.2f\n", total)
}


Here, the Order struct has multiple responsibilities: calculating the total amount and printing the order. To adhere to SRP, we should separate these concerns:


package main

import "fmt"

type Order struct {
	ID    int
	Items []string
}

type OrderCalculator struct{}

func (oc *OrderCalculator) CalculateTotal(o *Order) float64 {
	total := 0.0
	for _, item := range o.Items {
		total += 10.0 // Simplified: Assume each item costs $10
	}
	return total
}

type OrderPrinter struct{}

func (op *OrderPrinter) PrintOrder(o *Order) {
	fmt.Printf("Order #%d: %v\n", o.ID, o.Items)
}

func main() {
	order := &Order{ID: 1, Items: []string{"item1", "item2"}}
	calculator := &OrderCalculator{}
	total := calculator.CalculateTotal(order)

	printer := &OrderPrinter{}
	printer.PrintOrder(order)
	fmt.Printf("Total: $%.2f\n", total)
}


Each struct has a single responsibility, making the code easier to maintain and extend.


Open/Closed Principle (OCP)


The Open/Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code.


Example


Let's extend the order processing example. Suppose we want to add a discount feature. Following OCP, we should add new functionality without modifying existing code.

First, define an interface for discount strategies:


package main

type DiscountStrategy interface {
	ApplyDiscount(total float64) float64
}


Then, create concrete implementations for different discount strategies:


package main

type NoDiscount struct{}

func (d *NoDiscount) ApplyDiscount(total float64) float64 {
	return total
}

type PercentageDiscount struct {
	Percentage float64
}

func (d *PercentageDiscount) ApplyDiscount(total float64) float64 {
	return total * (1 - d.Percentage/100)
}


Modify the OrderCalculator to use a discount strategy:


package main

import "fmt"

type Order struct {
	ID    int
	Items []string
}

type DiscountStrategy interface {
	ApplyDiscount(total float64) float64
}

type NoDiscount struct{}

func (d *NoDiscount) ApplyDiscount(total float64) float64 {
	return total
}

type PercentageDiscount struct {
	Percentage float64
}

func (d *PercentageDiscount) ApplyDiscount(total float64) float64 {
	return total * (1 - d.Percentage/100)
}

type OrderCalculator struct {
	Discount DiscountStrategy
}

func (oc *OrderCalculator) CalculateTotal(o *Order) float64 {
	total := 0.0
	for _, item := range o.Items {
		total += 10.0 // Simplified: Assume each item costs $10
	}
	return oc.Discount.ApplyDiscount(total)
}

type OrderPrinter struct{}

func (op *OrderPrinter) PrintOrder(o *Order) {
	fmt.Printf("Order #%d: %v\n", o.ID, o.Items)
}

func main() {
	order := &Order{ID: 1, Items: []string{"item1", "item2"}}
	calculator := &OrderCalculator{Discount: &PercentageDiscount{Percentage: 10}}
	total := calculator.CalculateTotal(order)

	printer := &OrderPrinter{}
	printer.PrintOrder(order)
	fmt.Printf("Total after discount: $%.2f\n", total)
}


Now, we can easily add new discount strategies without modifying the OrderCalculator class.


Liskov Substitution Principle (LSP)


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 Go, this translates to ensuring that implementations of an interface can be used interchangeably.


Example

Consider a shape interface:


package main

import "fmt"

type Shape interface {
	Area() float64
}

type Rectangle struct {
	Width  float64
	Height float64
}

func (r *Rectangle) Area() float64 {
	return r.Width * r.Height
}

type Circle struct {
	Radius float64
}

func (c *Circle) Area() float64 {
	return 3.14 * c.Radius * c.Radius
}

func PrintArea(s Shape) {
	fmt.Printf("The area is %.2f\n", s.Area())
}

func main() {
	r := &Rectangle{Width: 10, Height: 5}
	c := &Circle{Radius: 7}

	PrintArea(r)
	PrintArea(c)
}


Both Rectangle and Circle implement the Shape interface, and we can substitute one for the other without any issues, adhering to LSP.


Interface Segregation Principle (ISP)


The Interface Segregation Principle states that a client should not be forced to depend on interfaces it does not use. In Go, this means creating small, specific interfaces rather than a large, general-purpose one.


Example

Suppose we have a printer and scanner interface. Instead of having a single interface for both functionalities, we can split them:

package main

import "fmt"

type Printer interface {
	Print(document string)
}

type Scanner interface {
	Scan() string
}

type SimplePrinter struct{}

func (p *SimplePrinter) Print(document string) {
	fmt.Println("Printing document:", document)
}

type SimpleScanner struct{}

func (s *SimpleScanner) Scan() string {
	return "Scanned document content"
}

func main() {
	printer := &SimplePrinter{}
	scanner := &SimpleScanner{}

	printer.Print("My Document")
	fmt.Println(scanner.Scan())
}


This way, clients can depend only on the interfaces they need, following the ISP.


Dependency Inversion Principle (DIP)


The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details. Details should depend on abstractions.


Example


Let's modify our order processing example to use dependency inversion. We'll create an interface for storage:


package main

import "fmt"

type Order struct {
	ID    int
	Items []string
}

type OrderStorage interface {
	Save(order *Order)
}

type DatabaseStorage struct{}

func (ds *DatabaseStorage) Save(order *Order) {
	fmt.Printf("Saving order #%d to the database\n", order.ID)
}

type OrderProcessor struct {
	Storage OrderStorage
}

func (op *OrderProcessor) Process(order *Order) {
	op.Storage.Save(order)
}

func main() {
	order := &Order{ID: 1, Items: []string{"item1", "item2"}}
	storage := &DatabaseStorage{}
	processor := &OrderProcessor{Storage: storage}

	processor.Process(order)
}


Depending on the OrderStorage interface, OrderProcessor doesn't need to know the details of how orders are saved. This makes it easy to swap out different storage mechanisms if needed.



Conclusion


The SOLID principles are fundamental guidelines for designing robust, maintainable software. By following these principles in Go (Golang), you can create easier code to understand, extend, and maintain. We hope this blog has provided a clear and practical introduction to SOLID principles with examples in Go. Happy coding!

Comments


Loading...

Post a Comment

Address

Nirvana Apt, Hinjeadi, Pune, Maharastra - 411057 (India)

Website
Site : www.anucodes.in
Social