Abstractions should not depend upon details. Details should depend upon abstractions. High-level modules should not depend on low-level modules; both should depend on abstractions.
Put another way, Abstract Classes should not depend on concrete classes; rather, a concrete class should depend on an abstract class.
The first half of the principle states that abstractions should not depend on details. This means that we should never do this:
# app_dip.py
class FrontEnd:
def __init__(self, back_end):
self.back_end = back_end
def display_data(self):
data = self.back_end.get_data_from_database()
print("Display data:", data)
class BackEnd:
def get_data_from_database(self):
return "Data from the database"The issue here is that FrontEnd depends on the existence of an instance of BackEnd to work. In other words, both classes are tightly coupled, and leads to issues of scalability and extensibility: if we want to change where the BackEnd gets its data from (say, from a SQL database to a REST API) we would have to modify both classes - BackEnd would need a new method for APIs, and FrontEnd will need to change its own methods, thus violating the Open-Closed Principle (OCP) since FrontEnd should not require modification, and the Single Responsibility Principle (SRP), since we would have many reasons to modify FrontEnd each time we add a new BackEnd method.
To fix this, we implement the second part of the principle: details should depend on abstractions. This is specifically accomplished by making FrontEnd use Dependency Injection, whereby that dependency refers to the abstract data source: interface:
# app_dip.py
from abc import ABC, abstractmethod
class FrontEnd:
def __init__(self, data_source):
self.data_source = data_source
def display_data(self):
data = self.data_source.get_data()
print("Display data:", data)
class DataSource(ABC):
@abstractmethod
def get_data(self):
pass
class Database(DataSource):
def get_data(self):
return "Data from the database"
class API(DataSource):
def get_data(self):
return "Data from the API"In this new version, FrontEnd is lightly modify to simply require an abstract “data source” plugin. What a data source itself is is defined in an interface called DataSource, which has concrete instances named Database and API. As a result, we can change the data source of the FrontEnd easily, without modifying the display_data method at all:
from app_dip import API, Database, FrontEnd
db_front_end = FrontEnd(Database())
db_front_end.display_data()
api_front_end = FrontEnd(API())
api_front_end.display_data()As we see, the concrete implementation of a DataSource doesn’t actually matter that much, so long it adheres to polymorphism. Note, too, that we are implementing good Encapsulation in the methods, since the client FrontEnd doesn’t need to know anything about how the data source is implemented.