Детали не имеют значения

Чтобы заставить программу работать, не требуется огромных знаний и навыков — Роберт С. Мартин

Каждая программная система предоставляет два значения: поведение и структуру.

Разработчики несут ответственность за то, чтобы программное обеспечение было не только работоспособным, но и чистым, удобочитаемым и легко изменяемым.

Вот где принципы SOLID пригодятся. Это маяки, которые помогают разработчикам создавать лучшие проекты и постоянно избегать ошибок в коде.

  • S: принцип единой ответственности
  • O: Принцип открытия-закрытия
  • L: принцип замены Лисков
  • I: принцип разделения интерфейса
  • D: принцип инверсии зависимостей

В этой статье я расскажу вам об одном из наиболее важных и часто используемых принципов — принципе инверсии зависимостей (DIP).

Что такое зависимость?

Прежде чем мы поговорим о DIP, стоит сначала уточнить, что именно является зависимостью.

def funcA():
   funcB()

Проще говоря, если function A вызывает function B, то function A зависит от function B.

Всякий раз, когда function B мутирует, function A будет склонен к изменениям и будет вынужден перекомпилировать.

Типичная программа выглядит так, как показано выше. Он начинается с функции main, которая вызывает некоторые функции высокого уровня, за которыми следуют функции среднего и низкого уровня.

В двух словах, инверсия зависимости означает инверсию направления зависимости.

Прежде чем мы углубимся в то, как, давайте поговорим о том, почему мы это делаем в первую очередь.

Почему ДИП?

Представьте себе приложение, которое запрашивает базу данных SQL и выводит данные на принтер.

Обработчик запрашивает данные и выводит их, вызывая функцию принтера.

Программа на первый взгляд кажется хорошей, но что, если мы хотим

  • Перейти с базы данных SQL на базу данных NoSQL?
  • Выводить данные в виде сообщения WhatsApp, а не печатать?

Нам неизбежно потребуется редактировать вызовы функций в обработчике и видоизменять часть логики, чтобы обеспечить совместимость данных.

Чтобы добавить что-то в болото, если у нас есть много обработчиков, вызывающих такие функции, нам нужно будет отредактировать их все! Простое требование вносит гигантские изменения в кодовую базу.

Тут на помощь приходит DIP.

Обработчик должен заботиться только о получении данных где-то и их выводе где-то. Он не должен ничего знать о деталях.

Детали НЕ имеют значение, и точка.

Функции высокого уровня не должны зависеть от модулей низкого уровня; оба должны зависеть от абстракций.

Что такое ДИП?

Как уже упоминалось, DIP означает инвертирование направления зависимости. Это достигается добавлением стабильного абстрактного интерфейса между вызывающим и вызываемым.

Вместо прямого вызова базы данных или принтера обработчик вызывает интерфейс. Интерфейс — это политика, определяющая сигнатуры методов — аргументы и выходные данные.

Обработчик вызывает интерфейс, не зная деталей. Он заботится исключительно о вводе и выводе, в то время как низкоуровневые функции заботятся о деталях и реализуют интерфейс.

Обратите внимание, что инверсия касается не только зависимостей, но и владения интерфейсом. Функции высокого уровня определяют интерфейс.

Реализация низкоуровневых функций зависит от того, как высокоуровневые функции определяют интерфейс. Следовательно, зависимость обратная!

Примеры

Давайте рассмотрим некоторые фрагменты кода, чтобы лучше понять, что такое DIP.

class SqlDb:
    def get(self):
        print("Getting data from sql db")
def main():
    sqlDb = SqlDb()
    data = sqlDb.get()

Без DIP мы определяем класс SqlDb и вызываем его непосредственно в main. В этом случае main зависит от SqlDb. Любое изменение в SqlDb потенциально потребует изменения в main.

В приведенном выше примере мы определяем абстрактный класс — DataInterface, который определяет политики. Это означает, что классы должны реализовать функцию get, чтобы стать DataInterface.

SqlDb и NoSqlDb оба реализуют DataInterface, имея функцию get.

Функция main вызывает getDataHandler для получения DataInterface . Ему все равно, что такое dataHandler. Все, о чем он заботится, это то, что dataHandler должен реализовать функцию get и вернуть набор данных.

Если бы мы переключились на новую БД, скажем, firebase, нам просто нужно было бы создать новый класс Firebase, реализовать функцию get и заменить ее в getDataHandler. Функция main никак не пострадает!

Обычно это также известно как принцип открытого-закрытого.

Преимущества

Преимущества DIP уже должны быть вам очевидны. Давайте рассмотрим некоторые из распространенных, прежде чем мы закончим пост.

Изменения в деталях не повлияют на бизнес-логику

Детали изменчивы. Мы можем изменить реализацию нашей базы данных или вывод в любое время.

Интерфейсы менее изменчивы, чем реализации.

Изменение реализации не обязательно требует изменения интерфейсов.

Следовательно, мы можем добавить больше функций в наши реализации, не затрагивая вызывающую их бизнес-логику.

Отложите решения о деталях

DIP позволяет бизнес-логике ничего не знать о деталях. Следовательно, это дает нам больше времени для реализации деталей позже.

Чем дольше мы ждем, чтобы принять эти решения, тем больше у нас будет информации, чтобы принять их правильно!

Хороший архитектор максимизирует количество невыполненных решений — Роберт С. Мартин

Через интерфейс мы можем отложить выбор базы данных на более поздний этап, временно используя локальное хранилище. Реализация может быть изменена в будущем, не затрагивая бизнес-логику.

Хорошие архитекторы разрабатывают политику таким образом, чтобы решения о деталях можно было откладывать и откладывать как можно дольше.

Заключение

Вот и все о принципе инверсии зависимостей! На мой взгляд, это одна из самых важных и фундаментальных концепций из 5 принципов SOLID.

Я надеюсь, что вы найдете этот пост полезным, и до встречи в следующем, Чао!

Чистый код всегда выглядит так, как будто его написал кто-то, кому не все равно — Роберт С. Мартин