В современной разработке программного обеспечения важно создавать гибкий и поддерживаемый код, который легко адаптируется к изменениям и расширяется с минимальными затратами.
Одной из основных проблем, с которыми разработчики сталкиваются, является зависимость модулей верхнего уровня от модулей нижнего уровня. Это приводит к тесной связи и усложнению процесса изменения кода. Здесь на помощь приходит принцип инверсии зависимостей (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 имеет свои ограничения и не всегда применим. Его следует использовать там, где это уместно, и всегда с учетом контекста и требований проекта.
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.