5 min read
SOLID in GO
This writing is after thought of seeing the SOLID pattern in GoLang as discussed in this blog.
Solid is the holy grail of software writing patterns and approach. It has 5 Principles, namely:
- S, Single Responsibility Principle
- O, Open Closed Principle
- L, Liskov Substitution Principle (LSP)
- I, Interface Segregation Principle
- D, Dependency Inversion Principle
I am not going to discuses the definition or concept of these principles, they are best justified in the blog mentioned above.
We are going to discuses how they look like in Go. As i feel, languages like Go, which are not hard bind OOP, or have intense OOP language pattern like Class etc. This means applying and seeing these pattern a non-intuitive or I can say non obvious.
What I just said, can be completely hoax that just my minds thinks.
Single Responsibility Principle.
A unit / module should have only one reason to change.
Every piece of entity function, struct, module should have single Concern. I.e they should be dealing with one motive. And clear and single responsibility.
How to find if your entity adhere to SRP.
Count the number reasons which can change the code.
Example:
⚠️ This is the example which the blog got it wrong.
type Order struct {
items []string
}
func (o *Order) addItem(item string) {
o.items = append(o.items, item)
}
func (o *Order) removeItem(item string) {
newItemsList := []string{}
for _, i := range o.items {
if i != item {
newItemsList = append(newItemsList, i)
}
}
o.items = newItemsList
}
func (o *Order) calculatePrice() {
return len(o.items) * 100
}The blog says this example is an example of SRP. But I dont think so, since numbe of reasons this module, say order.go need to change when:
- Managing order items (adding/removing items)
- Calculating pricing (
calculatePricemethod)
The correct way to write this would have been:
// order.go
type Order struct {
items []string
}
func (o *Order) AddItem(item string) {
o.items = append(o.items, item)
}
func (o *Order) RemoveItem(item string) {
newItemsList := []string{}
for _, i := range o.items {
if i != item {
newItemsList = append(newItemsList, i)
}
}
o.items = newItemsList
}
func (o *Order) GetItems() {
return o.items
}
// price_calculator.go
type PriceCalculator struct {
priceValue int
}
func NewPriceCalculator(value int) PriceCalculator {
return &PriceCalculator{ priceValue: value }
}
// order will be dependency to
func (pc PriceClculator) calcualtePrice(order Order) {
return order.GetItems() * pc.priceValue
}Here we don’t need to change the order.go / Order management login when providing customer with 🎇 Diwali Sales discount prices.
Big Open Source Project has something called Maintainers, where depending on directory changes different set of maintainers are auto requested for review. So you can really think in that point of view.
Open/Closed Principle (OCP)
Definition: Software entities (classes, modules, functions) should be open for extension, but closed for modification.
This is my favorite principle in SOLID, on future edits in code, it feels very present to work on, primarily due its plugin / extension kind of flow.
Example from that blog:
func processPayment(provider string) error {
switch provider {
case "stripe":
// stripe.API.POST(...
case "paytm":
// paytem.API.POST(...
default:
return errors.New("not valid providr")
}
}
This does not follow the guidelines of Open Closed Principle, since the function processPayment is:
- Open for modification
- Closed for extension
Liskov Substitution Principle (LSP)
Definition: If S is a subtype of T, then objects of type T should be replaceable with objects of type S without breaking the program.
Dependency Inversion Principle (DIP)
Definition: 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.
In simple English: Your business logic (high-level) shouldn’t directly depend on concrete classes (low-level). Instead, both should depend on interfaces (abstractions).