Samy's blog

Today I Learned: SOLID - Dependency Inversion Principle (DIP)

Today I Learned: SOLID - Dependency Inversion Principle (DIP)

So Continuing on my list of learning about the SOLID principles:

Today I'm going to write about the one that starts with a D, The fifth and last principle says the following: Depend on abstractions, not on concretions , or in other words:

Entities must depend on abstractions, not on concretions. A high-level module must not depend on the low-level module, but it should depend on abstractions.

Put more simply, higher-level modules should not care about the intrinsics of low-level modules.

Example time: Suppose we have a low level struct that represents a connection to a database, something like PostgresConnection like this, like this:

//Low-Level Module
type PostgresConnection struct {
	
}
func(pgc PostgresConnection ) Connect() error{
  //do stuff to connect
	fmt.Println("connecting!"
	
    return nil
}

and a high-level struct Shop that uses this struct, we also create a NewShop function that serves as a constructor of sorts:

//High-Level Module
type Shop struct {	
   connection PostgresConnection
}
func NewShop(connection PostgresConnection ) Shop {
       return Shop{connection}	
}

All good, except that: what if I want to swap to a MongoDB connection / Database in the future? or any other DB engine? we would have to edit the Shop struct and NewShop function, and this would violate the Open-Closed Principle. issues like these are the ones the Dependency inversion Principle shields us from, by inverting the dependency upwards upon an abstraction, let us see how we can represent this:

First, let us start by extracting the Connect functionality to an abstract entity, in our case a DBConnection interface

type DBConnection interface {
   Connect() error
}

next, let us update our High-Level Module Shop struct and our NewShop function so they can now depend on the abstraction (DBConnection) instead of the concrete PostgresConnection struct.

func NewShop(connection DBConnection) Shop {
   return Shop{connection}	
}

type Shop struct {	
   connection DBConnection
}

by definition, our previously defined PostgresConnection struct already 'Implements' the DBConnection and can be passed down to NewShop without any issues, but the best thing comes if we want to use another type of connection, like mentioned before, what would happen if we need a MongoDBConnection?

type MongoDBConnection struct {	
}

func(mgd MongoDBConnection) Connect() error{
   //do stuff to connect
   fmt.Println("connecting mongo!")
	
    return nil
}
func main() {
	shop1 := NewShop( PostgresConnection{}) //this works
	shop2 := NewShop( MongoDBConnection{}) //also works
	shop1.connection.Connect()
	shop2.connection.Connect()
}

well, not much changed!, and we were not forced to change anything on our Shop struct or NewShop function, we just needed to create a new struct that implementsDBConnection! , perfect face man cartoon fictional character hand nose art finger head forehead mouth human

The full example can be found here

I hope anyone who is reading my short learning summaries finds them useful somehow...and as always:

A bit of tinkering a day keeps the boredom away. 🚀