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

Почему это происходит?

Проведя небольшое исследование, я нашел объяснение дяди Боба, которое помогло мне понять, почему это может произойти. Он также научил меня тому, насколько важно применять принцип инверсии зависимостей.

Давайте начнем с понимания того, как класс зависит от другого класса и взаимодействует с ним. Допустим, у нас есть класс M и класс N.

Классу M нужна функция из класса N. Итак, нам нужно импортировать N в M, а затем вызвать f через N. Это означает, что M зависит от N, что дядя Боб называет «зависимостью исходного кода». Это означает, что если f изменится, M тоже нужно будет изменить. В дополнение к этой зависимости, от M до N также является потоком управления для вызова f. Итак, в M мы вызываем N, а затем можно вызвать f. Это называется «управление потоком».

Это обычное дело, которое мы делаем, когда пишем код. Но что произойдет, если мы применим это к более широкому контексту, например ко всему приложению? Вот пример:

Типичное приложение будет иметь такую ​​структуру. Чем ниже класс, тем конкретнее нужно что-то делать. Это неплохо, поскольку помогает нам разделить задачи (S в SOLID). Однако проблема в том, что класс напрямую зависит от другого класса, как в примере M и N выше. В этом случае M зависит от N (зависимость исходного кода). Это означает, что любые изменения в классе N потенциально могут потребовать изменения класса M, особенно если они связаны с функцией f. Вот что может случиться, когда мы изменяем один фрагмент кода, и это вызывает ошибки в других частях проекта. В случае изменения N необходимо изменить не только M, но и код, связанный с N, указанным выше, также необходимо будет изменить.

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

Как нам решить эту проблему?

Ответ — принцип инверсии зависимостей, блестящая концепция, которая помогает нам управлять зависимостями между компонентами нашего кода.

Принцип инверсии зависимостей (DIP) — один из пяти принципов SOLID, предложенных Робертом К. Мартином (дядей Бобом). Этот принцип гласит, что:

  • Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Я объясню это, используя контекст выше. Давайте вернемся к рассмотрению структуры управления потоком и зависимости M и N.

Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций. В этом контексте предполагается, что M является модулем высокого уровня. Исходя из этого, M и N не должны иметь никакой зависимости согласно DIP. Оба должны зависеть от абстракции. Абстракцию в некоторых языках программирования также часто называют интерфейсом, протоколом или абстрактным классом. Если проиллюстрировать, это будет так:

Зависимый означает, что зависимости исходного кода будут указывать на I для обоих.

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

А как насчет потока управления? Если мы только перемещаем зависимость и напрямую реализуем абстракцию, то M и N должны оба реализовывать f.

Если вы не знакомы с тем, как работают абстракции, позвольте мне объяснить. Когда класс реализует абстракцию или интерфейс, он должен обеспечивать реализацию методов или свойств, определенных в абстракции. Вот почему в M и N нам необходимо реализовать функцию f.

Бывают случаи, когда нам нужно использовать функцию f с особыми возможностями, которые определены в модуле низкого уровня, например N. В этом случае реализация интерфейса в M будет неэффективной, так как нам потребуется реализовать функцию f в M.

Чтобы решить эту проблему, мы можем использовать шаблон проектирования, называемый внедрением зависимостей. Внедрение зависимостей позволяет нам внедрить зависимости, необходимые классу, в класс, вместо того, чтобы класс сам принимал зависимости. Это означает, что мы можем внедрить экземпляр класса N в M, который предоставит функцию f, необходимую M.

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

Как это реализовать в реальном случае?

Представьте, что мы работаем над созданием функции оплаты для приложения. Будет задействовано два модуля: модуль платежного процессора и модуль платежного шлюза. Модуль платежного процессора будет отвечать за обработку платежа, а модуль платежного шлюза будет отвечать за обработку типа платежа.

Не используя концепцию принципа инверсии зависимостей, мы обязательно соединим PaymentProcessor и PaymentGateway напрямую. И зависимость исходного кода, и управление потоком приведут непосредственно к PaymentGateway.

Это будет хорошо работать, если существует только один тип PaymentGateway. Предположим, что текущий платежный шлюз осуществляется через PayPal. Если однажды мы захотим заменить или обновить способ обработки платежей системой (еще один платежный шлюз), нам придется изменить код непосредственно внутри PaymentProcessor, что может повлиять на многие части другого кода, зависящего от PaymentProcessor.

Если мы хотим иметь два платежных шлюза, а именно PayPal и CreditCard, нам необходимо добавить их в платежный процессор, что означает повторное изменение кода платежного процессора.

Предоставленная реализация, безусловно, будет работать хорошо, но у нее есть несколько ограничений.

Во-первых, нам нужно изменить имя модуля с PaymentGateway на более конкретное, например CreditCardPayment и PayPalPayment.

Во-вторых, класс PaymentProcessor теперь напрямую зависит от двух других модулей: CreditCardPayment и PayPalPayment. Это может стать проблемой, если мы захотим добавить еще один платежный шлюз в будущем.

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

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

Если вернуться в контекст приложения выше. Зависимость исходного кода изменится следующим образом

Когда дело доходит до управления потоком, в этом случае мы можем снова использовать концепцию внедрения зависимостей. Используя внедрение зависимостей, мы внедряем необходимые зависимости в класс, вместо того, чтобы класс сам принимал зависимости. Это обеспечивает гибкость и замену реализации без нарушения остального кода. Таким образом, управление потоком по-прежнему может осуществляться обоими модулями.

Класс PaymentProcessor будет зависеть от PaymentGatewayInterface. Это означает, что мы можем легко изменить платежный шлюз без необходимости изменения класса PaymentProcessor.

Например, если мы хотим переключиться на шлюз оплаты кредитной картой, мы можем просто внедрить экземпляр класса CreditCardPaymentGateway в класс PaymentProcessor. Это вообще не потребует от нас изменения класса PaymentProcessor.

То же самое применимо, если мы хотим добавить еще один способ оплаты, например платежный шлюз с помощью дебетовой карты. Мы можем просто создать новый класс, реализующий PaymentGatewayInterface, а затем внедрить экземпляр нового класса в класс PaymentProcessor.

Это делает код более гибким и адаптируемым к изменениям. Мы можем легко добавлять новые функции или изменять существующие функции без необходимости изменения большого количества кода.

По моему мнению, протокол инверсии зависимостей становится очень важным, особенно в некоторых языках, которые все еще четко выполняют процесс компиляции по сравнению с интерпретируемыми языками. Например, как C++ или другие языки. Это неэффективно, если вам нужно скомпилировать более 10 других файлов только потому, что вы изменили 1 файл.

Это то, что часто называют «зависимостями времени компиляции». Без реализации DIP при изменении модуля необходимо перекомпилировать другие модули, зависящие от него, что может занять значительное время и ресурсы, особенно в крупных проектах.

Поздравляем!

Добро пожаловать в конец урока! Поздравляю с тем, что уже многому научился.

Во-первых, вы узнали, что такое принцип разделения интерфейса (DIP). Этот принцип гласит, что:

  • Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

В-третьих, вы узнали, как реализовать DIP в реальном случае и применить его к функции оплаты.

Вы все еще в замешательстве?

Возможно, объяснение этой концепции до сих пор оставило у вас много вопросов. Но поверьте, это нормально. Вы поймете это со временем. Чтобы понять эту концепцию, вам также необходимо знать ее реализацию в коде. Для этого я написал еще одну статью специально о реализации кода. Вы можете получить к нему доступ через: https://mrezkys.medium.com/dependent-inversion-principle-d-in-solid-practical-with-swift-3d5da3757172

От писателя

Здравствуйте, разрешите представиться — я Мухаммад Резкий Сулихин. Мы подошли к концу этой статьи, и я искренне благодарю вас за то, что нашли время ее прочитать. Если у вас есть какие-либо вопросы или отзывы, свяжитесь со мной напрямую по электронной почте [email protected]. Я более чем рад получить ваше мнение, будь то мои навыки письма на английском языке или что-то еще, я могу ошибаться. Ваши идеи помогут мне расти.

С нетерпением ждем возможности связаться с вами в будущих статьях! Кстати, я мобильный разработчик, сейчас учусь в Apple Developer Academy. Я открыт для различных возможностей, таких как сотрудничество, внештатная работа, стажировки, неполный или полный рабочий день. Для меня было бы огромным счастьем изучить эти возможности.

До следующих встреч, сохраняйте любопытство и продолжайте учиться!