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:
- S - Single-responsiblity Principle
- O - Open-Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D- Dependency Inversion Principle (you are here)
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
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. 🚀