Здравствуйте, давно читаю разные туториалы и много где встречаю должно быть мало зависимостей, как я понял это все достигается при помощи interface и IoC. Нашел много примеров но так сути и не понял как правильно это достигать... К примеру у меня есть проект с моделями(таблицы из БД) и сервисы(классы для выполнения каких=либо действий над ними), и что получается мне для каждого сервиса писать интерфейс его действий? Я понимаю что если добавится другой класс мы просто имплементим его и ничего менять не нужно практически. Но с другой стороны много лишнего кода и классов. Может быть мне кто-нибудь сможет подробно объяснить как достигнуть минимизации зависимостей на примерах кода Java или просто на словах, чтобы я уловил смысл?
Ответ
Для чего вообще нужно минимизировать зависимости? Фредерик Брукс, автор статьи «Серебряной пули нет» утверждал, что производство программного обеспечения дело трудное, и в ближайшее время не появится ничего, что сделает его проще.
Это было в середине 80-х. Через 10 лет Брукс изучил, изменилось ли что-нибудь кардинально в индустрии. Оказалось, что нет. Правда, один из подходов выглядел многообещающе — повторное использование кода
Речь о том, что, написав большую систему, и приступая к написанию другой, мы могли бы взять готовые куски кода и использовать их повторно. Фактически, мы могли бы сократить работу минимум на 30%. Этому мешает то, что части системы сильно сцеплены (couple) друг с другом.
Для борьбы со сцепленностью придуманы без малого десятки техник. Часть из них дублируют друг друга на разных уровнях детализации. Вначале программы были не очень большими, и программисты делали независимыми отдельные функции. Скажем, предпочитали чистые функции, значение которых зависело только от аргументов, следовательно, их можно было безболезненно перенести в другой проект. Предпочитали модули с высокой связностью, но малой сцепленностью. В современных ОО языках предпочитают классы с единственной ответственностью.
Когда программы стали слишком большими, модули объединили в слои (уровни) и задали чёткое правило направления зависимостей: нижние уровни не могут зависеть от верхних. Никогда.
В классическом трёхзвенном приложении уровень представления зависит от уровня предметной области, а тот, в свою очередь, от уровня доступа к данным.
Presentation → Domain → Data Access
Для примера возьмём интернет-магазин. Одной из сущностей магазина является Заказ (Order). Этот класс принадлежит к уровню предметной области, потому что вся работа интернет-магазина строится вокруг Заказов. Если вы используете паттерн MVC в построении веб-приложения, то заведовать заказами будет Контроллер Заказов (OrderController). Этот класс принадлежит к уровню представления. Значит, класс OrderController может знать и использовать Order, а Order ничего про OrderController знать не может.
В этом есть глубокий смысл. Предположим, у вас работает не только веб-приложение, но фоновые сервисы, которые, по сути являются консольными приложениями. Они запускаются с заданным интервалом и что-то делают с заказами. Очень правильно в этом случае повторно использовать весь код, реализующий Заказы и их сохранение в БД. Но чтобы это сделать, надо чётко понимать, что является представлением, а что нет. OrderController реагирует за запросы HTTP, и у него есть такие штуки, как URI запроса, текущий пользователь и прочее. Но ничего этого нет у самого Заказа. Эта ошибка встречается часто: в классах предметной области хранятся дескрипторы окна, или данные, специфичные для веб-приложений. На самом деле класс Order не может строить никаких предположений о том, в какой среде его будут использовать.
Вопрос, трудно ли будет создать фоновые сервисы или оконные приложения, если приложение спроектировано правильно? Не очень трудно. Нам, конечно, придётся написать новый слой, но нам точно не нужно будет переписывать классы нижних уровней, в частности Order
Теперь опускаемся на уровень ниже, к данным. Предположим, все данные лежат в MySQL и мы для доступа к ним используем JDBC. SQL-запрос для постраничного списка заказов мы пишем сами, он в виде строковой константы находится в Java-коде:
SELECT Orders.*
FROM Orders
ORDER BY Orders.CreatedAt
LIMIT ? OFFSET ?
На уровне доступа к данным у нас находятся классы Connection, ResultSet, Statement из пространства имён java.sql. Классы с верхних уровней могут к ним обращаться, то есть Order мог бы уметь создавать себя из ResultSet, а OrderController мог бы выполнять запросы с помощью Statement
Снова всё хорошо. Вопрос, трудно ли будет изменить способ хранения с MySQL на Oracle, или даже на что-нибудь вроде MongoDB? На этот раз гораздо труднее, чем раньше.
Нам придётся вносить изменения не только в классы нижнего уровня, но и в Order, и в OrderController. В Oracle постраничный доступ требует другого синтаксиса, а для загрузки данных из MongoDB уже нельзя использовать ResultSet
Решение заключается в том, чтобы в данном месте инвертировать зависимость. Мы говорим, что не знаем заранее, как будем обращаться к данным. Вместо конкретных классов ResultSet и MongoCollection мы скажем: у нас точно будет какое-то внешнее хранилище из которого мы захотим постранично получать данные. Наверное, нам придётся узнавать и общее количество страниц.
public interface OrderRepository {
List ReadAll(int oneBasedPageNumber);
int ReadTotalPages();
}
Интерфейс Хранилище Заказов (OrderRepository) находится на уровне предметной области, и OrderController может использовать его. Реализация интерфейса для MySQL находится на уровне доступа к данным, но теперь зависимость инвертирована: доступ к данным зависит от предметной области.
public MysqlOrderRepository implements OrderRepository {
@Override
public List ReadAll(int oneBasedPageNumber) {
. . .
}
}
Реализация MysqlOrderRepository видит Order и может создавать объекты Заказа. Зависимости теперь выглядят так:
Presentation → Domain ← Data Access
Из всего изложенного становятся понятно, что использовать интерфейсы для доступа к объектам предметной области не нужно, если этого не требуют специальные условия. Скажем так, Заказ в интернет-магазине — одна из базовых сущностей, и вряд ли вам потребуют две конкурирующие реализации Заказов. Точно также и сервисы предметной области интерфейсов не требуют. Бизнес-процесс оформления заказа фиксирован, поэтому его сразу можно реализовать в виде конкретного класса Сервис Оформления Заказов (OrderService).
Использовать интерфейсы для объектов уровня представления также не нужно, потому что они находятся на самом верху и от них ничего не зависит. Интерфейсами имеет смысл закрывать только объекты и сервисы уровней ниже предметной области. Мы обозначили их как уровень Data Access, а Эрик Эванс, создатель DDD, предлагает называть их инфраструктурными. Соответственно, у нас может быть сервис рассылки электронных сообщений NotificationService. Это интерфейс уровня предметной области, чья реализация TwilioNotificationService находится на инфраструктурном уровне.
При такой организации проекта мы получаем возможность быстро клонировать проект и дописать новый кусок. Хотим сделать консольное приложение? Берём готовые уровни предметной области и инфраструктуры, дописываем обвязку консольного приложения. Хотим перенести на СУБД Postgres? Берём готовые уровни предметной области и представления, дописываем реализацию хранилищ на PostreSQL. Зависимости остаются, но они упорядочены и позволяют изымать и подменять целые уровни приложения.
Есть несколько случаев, когда интерфейсы могут появиться на высоких уровнях. Скажем, если в интернет-магазине могут быть разные способы начисления скидок, их удобно реализовать в виде паттерна Стратегия. По сути это будет иерархия однотипных классов, каждый из которых рассчитывает скидку на основании своих данных. Общие методы этих классов можно вынести в базовый интерфейс. В этом случае необходимость в интерфейсе диктуется уже не зависимостями, а способом реализации конкретного паттерна.
Резюмирую: мы в действительности можем говорить о минимизации зависимостей, как об управлениями зависимостями. Мы не можем от них избавиться совсем, но мы можем их ограничивать. Для этого мы разбиваем приложение на слои, и вводим правило: все зависимости идут в одну сторону. Использовать можно только свои классы, либо классы слоя, от которого мы зависим. Ядром приложения, его солью является слой предметной области. Все слои, которые выше него, оставляем как есть, они и так от него зависят. Для слоёв, которые ниже него, инвертируем зависимости. На практике это означает, что мы вводим интерфейсы в слой предметной области, которые реализуем в нижних слоях.