Циклическая зависимость.
Циклическая зависимость — антипаттерн, при котором модули зависят друг от друга, и ни один из них нельзя собрать, протестировать или переиспользовать отдельно.
В программной инженерии циклическая зависимость (или круговая зависимость) — это отношение между двумя или более модулями, которые прямо или косвенно зависят друг от друга для корректной работы. Такие модули также называют взаимно рекурсивными.
Обзор
Циклические зависимости естественны во многих моделях предметной области, где определённые объекты одной предметной области зависят друг от друга. Однако в проектировании ПО циклические зависимости между более крупными программными модулями считаются антипаттерном из-за их негативных эффектов. Несмотря на это, такие циклические (или круговые) зависимости оказались широко распространены среди исходных файлов реального ПО. Взаимно рекурсивные модули, впрочем, довольно обычны в функциональном программировании, где индуктивные и рекурсивные определения часто поощряются.
Проблемы
Циклические зависимости могут вызывать множество нежелательных эффектов в программах. С точки зрения проектирования ПО наиболее проблематична тесная связанность взаимозависимых модулей, которая снижает или делает невозможным раздельное переиспользование отдельного модуля.
Циклические зависимости могут вызывать эффект домино, когда небольшое локальное изменение в одном модуле распространяется на другие модули и приводит к нежелательным глобальным эффектам (ошибкам программы, ошибкам компиляции). Циклические зависимости также могут приводить к бесконечной рекурсии или иным неожиданным сбоям.
Циклические зависимости могут также вызывать утечки памяти, мешая некоторым автоматическим сборщикам мусора (тем, что используют подсчёт ссылок) освобождать неиспользуемые объекты.
Причины и решения
В очень больших программных проектах инженеры могут терять контекст и непреднамеренно вводить циклические зависимости. Существуют инструменты для анализа ПО и поиска нежелательных циклических зависимостей.
Циклические зависимости могут возникать при реализации функциональности обратного вызова. Этого можно избежать, применяя паттерны проектирования, такие как паттерн «наблюдатель».
Пример
OrderService нужен Billing, чтобы оплатить заказ, а Billing обращается обратно к OrderService, чтобы пометить заказ оплаченным. Ни один из классов нельзя скомпилировать, протестировать или переиспользовать без другого.
class OrderService {
private final Billing billing = new Billing(this);
void place(Order order) { billing.charge(order); }
void markPaid(Order order) { order.paid = true; }
}
class Billing {
private final OrderService orders;
Billing(OrderService orders) { this.orders = orders; }
void charge(Order order) {
// ... списываем деньги с карты ...
orders.markPaid(order); // Billing обращается обратно к OrderService
}
}
Обратный вызов идёт через небольшой интерфейс, которым владеет модуль оплаты, как в паттерне Наблюдатель:
interface PaymentListener {
void paid(Order order);
}
class Billing {
private final PaymentListener listener;
Billing(PaymentListener listener) { this.listener = listener; }
void charge(Order order) {
// ... списываем деньги с карты ...
listener.paid(order);
}
}
class OrderService implements PaymentListener {
private final Billing billing = new Billing(this);
void place(Order order) { billing.charge(order); }
@Override
public void paid(Order order) { order.paid = true; }
}
Теперь Billing зависит только от PaymentListener, и зависимость между модулями направлена в одну сторону: заказы → оплата.
Адаптировано по статье Википедии “Circular dependency” (версия 1303252600), распространяется по лицензии CC BY-SA 4.0; текст переведён и отредактирован, добавлены примеры.