В современной разработке программного обеспечения важно создавать гибкий и поддерживаемый код, который легко адаптируется к изменениям и расширяется с минимальными затратами.
Одной из основных проблем, с которыми разработчики сталкиваются, является зависимость модулей верхнего уровня от модулей нижнего уровня. Это приводит к тесной связи и усложнению процесса изменения кода. Здесь на помощь приходит принцип инверсии зависимостей (Dependency Inversion Principle или DIP), который входит в состав принципов SOLID — основ объектно-ориентированного программирования.
В этом уроке мы рассмотрим применение принципа инверсии зависимостей и увидим, как он помогает создавать гибкий и расширяемый код.
Что такое DIP
Принцип инверсии зависимостей или Dependency Inversion Principle (DIP) — это один из принципов SOLID, которые являются основой объектно-ориентированного программирования.
SOLID — это акроним, который состоит из первых букв пяти принципов, а DIP — последний из них.
DIP гласит:
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций
В основе этого принципа лежит идея, что высокоуровневые модули, которые описывают правила бизнес-логики, не должны зависеть от низкоуровневых модулей, обеспечивающих выполнение базовых операций: чтение и запись в базу данных.
Это значит, что мы должны стараться делать наши модули независимыми друг от друга. Так мы можем легко менять одни модули без необходимости вносить изменения в другие.
Допустим, у нас есть класс User
, который использует класс MySQLDatabase
для сохранения информации о пользователях:
class MySQLDatabase:
def save(self, data):
print(f"Saving {data} to MySQL database")
class User:
def __init__(self):
self.database = MySQLDatabase()
def save_user(self, data):
self.database.save(data)
В этом примере класс User
зависит от класса MySQLDatabase
. Это означает, что если мы захотим заменить MySQLDatabase
на другую базу данных, нам придется изменить класс User
. Это нарушает DIP.
Чтобы исправить это, мы можем использовать абстракцию для создания общего интерфейса, который будет использоваться классом User
:
class Database:
def save(self, data):
pass
class MySQLDatabase:
def save(self, data):
print(f"Saving {data} to MySQL database")
class User:
def __init__(self, database):
self.database = database
def save_user(self, data):
self.database.save(data)
Теперь класс User
не зависит от конкретного класса MySQLDatabase
, а зависит от абстракции Database
. Это означает, что мы можем легко заменить MySQLDatabase
на другую базу данных, которая поддерживает интерфейс Database
. И в этом случае не нужно вносить какие-либо изменения в класс User
.
Пример добавления новой базы данных
Теперь представим, что мы хотим использовать NoSQL базу данных MongoDB вместо MySQL. Для этого нужно создать новый класс MongoDBDatabase
, который реализует метод save
, и передать его в класс User
:
class MongoDBDatabase:
def save(self, data):
print(f"Saving {data} to MongoDB database")
user = User(MongoDBDatabase())
user.save_user("user data")
Принцип инверсии зависимостей обеспечивает большую гибкость. Он позволяет легко заменить одну реализацию базы данных другой и не менять остальной код. Благодаря DIP наши классы становятся гибкими и адаптируемыми к изменениям.
Но есть еще способы инъекции зависимостей, которые помогают управлять зависимостями в нашем коде. Они позволяют передавать объекты, от которых зависит класс, через аргументы функции, конструктор или сеттеры. В каждом случае выбор метода инъекции зависимостей зависит от конкретного случая и требований к коду.
Способы инъекции зависимостей
Принцип инверсии зависимостей не только облегчает работу с кодом, но и открывает возможности для различных способов инъекции зависимостей. Всего существует три основных способа инъекции зависимостей:
Инъекция через аргументы функций или методов — наиболее прямой и простой способ инъекции зависимостей. Зависимости передаются в качестве аргументов функции или метода, который их использует:
def do_something_useful(logger): # some code do_something_useful(Logger())
В этом примере функция
do_something_useful
принимает объектlogger
как аргумент, который используется внутри функции.Инъекция через конструктор — когда мы работаем с объектами, мы можем передавать зависимости через конструктор объекта:
class Application: def __init__(self, logger): self.logger = logger app = Application(Logger())
Здесь зависимость
logger
передается в конструктор классаApplication
и сохраняется внутри объекта для последующего использования.Инъекция через сеттеры — этот метод связан с изменением состояния объектов и может нарушить их целостность, поэтому его следует использовать с осторожностью:
class Application: def set_logger(self, logger): self.logger = logger app = Application() app.set_logger(Logger())
В этом примере объект
logger
устанавливается в объектApplication
после его создания через специальный метод (сеттер)set_logger
.
За громким термином "инверсия зависимостей" скрывается очень простая штука — передача параметров. С другой стороны термины позволяют понять больше смысла без необходимости знать дополнительный контекст. Главное не увлекаться, а то можно превратиться в архитектурных астронавтов.
Выводы
Принцип инверсии зависимостей — важная часть SOLID. Он помогает писать более гибкий и поддерживаемый код. С помощью этого принципа мы можем делать модули независимыми друг от друга за счет уменьшения связанности в коде. Это позволяет легче вносить изменения в код и делает его более устойчивым к изменениям.
Важно помнить, что, как и любой принцип или паттерн проектирования, DIP имеет свои ограничения и не всегда применим. Его следует использовать там, где это уместно, и всегда с учетом контекста и требований проекта.
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.