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