Рубрика:
Разработка /
Принципы проектирования
|
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|
ВИЗИТКА
Ольга Федорова, технический лидер «Альфа банка»
Dependency Inversion Principle Принцип инверсии зависимостей в разработке
Мы подошли к последнему принципу проектирования приложений из серии SOLID – Dependency Inversion или «Инверсия зависимостей».
Как обычно, начнем с оригинальной формулировки Роберта Мартина, которая звучит следующим образом: модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Звучит прекрасно, но, как обычно, слишком теоретично. Давайте обратимся к конкретным примерам.
Предположим, у нас есть класс EmployeeManager, который отвечает за управление сотрудниками, включая добавление новых сотрудников и получение списка всех сотрудников. В начале реализации этот класс может зависеть напрямую от конкретной базы данных, например, PostgreSQL.
public class EmployeeManager { private PostgreSQLDatabase database; public EmployeeManager() { this.database = new PostgreSQLDatabase(); } public void addEmployee(Employee employee) { database.add(employee); } public List<Employee> getAllEmployees() { return database.getAll(); } }
Итак, все работает, все счастливы. В один непрекрасный день вы сталкиваетесь с необходимостью перейти на другую базу данных, например, MongoDB. Да, здесь можно задать вопрос, как получилось так, что внезапно с SQL- решения надо уходить на NoSQL? Но, как показывает практика, в жизни может быть все что угодно.
Например, некоторые команды используют PostgreSQL для хранения данных в формате jsonb, а разработчики базы вполне себе такие решения позволяют, так что подобная смена стека еще далеко не самый странный вариант.
Так, вот, возвращаясь к нашему примеру, конечно же, вы не можете допустить, чтобы ваш код зависел от конкретной реализации базы данных. Иначе с подобных архитектурных решений вам придется перепиливать половину продукта. Более того, отдельные зависимости, скорее всего уйдут куда-то вглубь программного решения и найти их будет не так уж просто. А заменить через пару лет активной разработки тем более.
Здесь мы и вспоминаем оригинальную формулировку Dependency Inversion. Суть заключается в том, чтобы модули верхнего уровня (например, EmployeeManager) не зависели от модулей нижнего уровня (например, PostgreSQLDatabase), а зависели от абстракций (интерфейсов). Это позволяет избежать жестких зависимостей и сделать систему более гибкой и поддерживаемой. В том числе обеспечив возможность легко проворачивать подобные замены практически незаметно для всей системы.
Рассмотрим следующий пример, чтобы показать, как можно использовать принцип Dependency Inversion. Для этого создадим интерфейс Database, который будет абстрагировать операции с базой данных.
public interface Database { void add(Employee employee); List<Employee> getAll(); }
Теперь изменим класс PostgreSQLDatabase – таким образом, чтобы он реализовывал интерфейс Database.
public class PostgreSQLDatabase implements Database { @Override public void add(Employee employee) { // Реализация добавления сотрудника в PostgreSQL }
@Override public List<Employee> getAll() { // Реализация получения всех сотрудников из PostgreSQL } }
Аналогично, создадим класс MongoDBDatabase, который также будет реализовывать интерфейс Database.
public class MongoDBDatabase implements Database { @Override public void add(Employee employee) { // Реализация добавления сотрудника в MongoDB }
@Override public List<Employee> getAll() { // Реализация получения всех сотрудников из MongoDB } }
Теперь изменим класс EmployeeManager, чтобы он зависел от интерфейса Database.
public class EmployeeManager { private Database database;
public EmployeeManager(Database database) { this.database = database; }
public void addEmployee(Employee employee) { database.add(employee); }
public List<Employee> getAllEmployees() { return database.getAll(); } }
Такой подход позволяет легко менять реализацию базы данных, не изменяя код класса EmployeeManager. А это положительно влияет и на уровень абстракций, способствуя появлению интерфейсов в нужных местах. Что, в свою очередь, как мы обсудили в предыдущей части статьи, невероятно важно для гибкой архитектуры приложения.
И, кроме того, чем меньше у вас жестко связанной логики, тем проще писать unit-тесты, в них не приходится использовать сложные моки. Да, много библиотек вам предоставят достаточно инструментов для создания моков любой сложности, но гораздо проще тестировать изначально класс с минимумом зависимостей.
Из полезных, но, возможно, не слишком очевидных примеров применения данного принципа в жизни, могу вспомнить такую вещь, как возможность динамически добавлять функциональность с помощью wrapper’ов. Например, поверх интерфейса запихать кэширование (здесь можно вспомнить стартеры Spring или любого другого похожего фреймворка).
Более того, как и другие принципы SOLID, Dependency Inversion применим не только на уровне отдельных классов или модулей, но и на уровне всей системы. Например, благодаря ему мы можем собрать / пересобрать проект любой сложности, просто переиспользуя уже заранее написанные адаптеры или их аналоги. Единственным исключением станут разве что классы-конфигурации.
Попробую продемонстрировать эту мысль на более жизненной ситуации. Допустим, пользователь изменил в банке свои настройки (вместо информирования по смс, захотел по email). Если инверсия зависимостей была реализована на системном уровне, то мы без особого труда можем собрать сервис из отдельных модулей. Переиспользуем всю ту же бизнес-логику, только на выходе сообщение будет отдельным модулем конвертироваться в письмо (а не в смску) и использоваться соответственно будет другая интеграция. В противном же случае, если все прибито гвоздями, скорее всего эта завязка на смс проходит красной нитью, а значит, перестраивать систему будет больно и долго.
В целом, на этом статью об инверсии зависимостей можно было бы заканчивать, если не одно но: DI – это не только Dependency Inversion, но еще и Dependency Injection. Если вы работаете на JVM- стеке, то с таким паттерном точно сталкивались не раз, это неотъемлемая часть экосистемы Spring. А заодно и прекрасный пример того, как, зачастую, в мире разработки не следуют собственному же принципу о понятном и удобном нейминге.
Dependency Inversion – это про разделение логики и внедрение абстракций. Dependency Injection – это про внедрение зависимостей, в рамках так называемого IoC (Inversion of Control). Основное отличие заключается в том, что DI, который (Dependency Injection) – это про контроль над зависимостями. Контроль, который можно осуществлять разными способами и не всегда вручную. Данную ответственность можно легко делегировать фреймворку. Мы еще вернемся к этой теме в будущих статьях.
А теперь предлагаю подвести итоги нашего цикла SOLID. Главное, что я хочу подчеркнуть: это в первую очередь не теоретическая аббревиатура, а системный подход достаточно высокого уровня абстракции. Каждый принцип логично дополняет другой и, несмотря на то, что любое архитектурное решение – это компромисс, использование SOLID определенно даст ряд преимуществ вашей разработке.
Ключевые слова: SOLID, DIP, Dependency Inversion Principle, Dependency Injection, Роберт Мартин
Подпишитесь на журнал
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|