SOLID: The Dependency Inversion Principle (DIP)

High level policy should depend on abstractions rather than low-level implementations.

August 12, 2021

Introduction

Robert C. Martin coined the mnomonic acronym SOLID to represent 5 principles of software design. This is the fifth and final installment of my own five-part series exploring my understanding of and experience applying these principles:

  1. [SRP] The Single-Responsibility Principle:
    • "Software entities should have only one reason to change."
  2. [OCP] The Open–Closed Principle:
    • "Software entities should be 'open for extension but closed for modification'."
  3. [LSP] The Liskov Substitution Principle:
    • "Subtypes must be substitutable for their base types."
  4. [ISP] The Interface Segregation Principle:
    • "Clients should not be forced to depend on methods they do not use."
  5. [DIP] The Dependency Inversion Principle: (you are here)
    • "High-level policy should depend upon abstractions, not concretions."

DIP

They say a picture is worth a thousand words:

Would you solder a lamp directliy to the electrical wiring in a wall?

No, of course we wouldn't solder an electrical appliance directly to the wiring in the wall. For. so. many. reasons! What do we do instead? We have established an interface: the wall socket. Appliances must 'implement' the wall socket interface by providing a pronged plug that fits in the socket and receives alternating electrical current.

What does this have to do with software?

EVERYTHING

Alternating electrical current is a low-level detail, and is dangerous to deal with directly. The interface protects users from harm, and makes using electrical power more convenient. There are things like this in software all over the place.

High-level business rules shouldn't depend directly on low-level utilities like database drivers, communications protocols, or other I/O concerns. Business rules should be kept aloof from such details so that the details can change independent of the rules. Right now you only need to deal with messages to/from a Rabbit MQ instance (AMQP protocol) message queue, but someday you might introduce NSQ or Apache Kafka...

Bad (high-level policy depending directly on low-level details):


+--------------+
|              |
|  High-Level  |
|    Policy    +-------------+
|              |             |
+--------------+             |
                             |
                             |
                             v
                       +-----------+
                       |           |
                       | Low-Level |
                       |   Detail  |
                       |           |
                       +-----------+

Good (high-level policy depends on interface, which is implemented by any of various low-level components):


+--------------+
|              |      +-----------+
|  High-Level  |      |           |
|    Policy    +----> | Interface |
|              |      |           |
+--------------+      +-----------+
                             ^
                             |
                             |
                       +-----+-----+
                       |           |
                       | Low-Level |
                       |   Detail  |
                       |           |
                       +-----------+

Bad (thermostat depends directly on specific HVAC driver):

type Thermostat struct {
	hvac *SuperHVAC3000
}

func NewThermostat(hvac *SuperHVAC3000) *Thermostat {
	return &Thermostat{hvac: hvac}
}

Good (Thermostat depends on interface, which a [probably wrapped] low-level driver must implement):

type HVAC interface {
	Heat()
	Cool()
	Vent()
}

type Thermostat struct {
	hvac HVAC
}

func NewThermostat(hvac HVAC) *Thermostat {
	return &Thermostat{hvac: hvac}
}

Why? Well, for all the reasons mentioned above. Safety, flexibility, more options in the future, etc... But also:

TESTABILITY

func TestThermostat(t *testing.T) {
	hvac := &FakeHVACForTesting{}
	thermostat := NewThermostat(hvac)
	...
}

Without the HVAC interface, you'd probably have to fire up an actual HVAC hardware system. Not fun. Not fast. Not gonna happen.