A Alegria das Dependências Circulares
Depois de 47 anos acoplando fortemente cada módulo existente, descobri que dependências circulares não são code smell—são uma feature. Elas mostram que seus módulos realmente se entendem.
O Que é uma Dependência Circular?
É quando o Módulo A precisa do Módulo B, e o Módulo B precisa do Módulo A. Desenvolvedores chatos chamam isso de “problema.” Eu chamo de companheirismo.
UserService ←→ OrderService ←→ PaymentService
↑ ↓ ↑
└──────────────┴────────────────┘
Veja como tudo está conectado? Isso é o que arquitetura sênior parece.
A Beleza da Necessidade Mútua
Como o XKCD 754 mostra, dependências são inevitáveis. Por que lutar contra elas? Abrace o círculo. Módulo A depende de B, B depende de C, C depende de A. É o ciclo da vida, Simba. O ciclo de vida do código.
# user_service.py
from order_service import OrderService
from payment_service import PaymentService
class UserService:
def get_user_orders(self, user_id):
return OrderService().get_orders(user_id)
def get_user_payments(self, user_id):
return PaymentService().get_payments(user_id)
# order_service.py
from user_service import UserService
from payment_service import PaymentService
class OrderService:
def get_orders(self, user_id):
user = UserService().get_user(user_id) # Preciso das info do user!
payments = PaymentService().get_order_payments(user.orders)
return user.orders
# payment_service.py
from user_service import UserService
from order_service import OrderService
class PaymentService:
def process_payment(self, order_id):
order = OrderService().get_order(order_id)
user = UserService().get_user(order.user_id)
# Agora temos TUDO que precisamos!
return self._charge(user, order)
Lindo. Tudo sabe de tudo. Sem necessidade de passar dados por aí—só importe o que você precisa!
Por Que Dependências Circulares São Na Verdade Boas
1. Segurança no Emprego
Ninguém consegue entender seu código exceto você. Como o Dogbert do Dilbert uma vez disse: “O caminho para a segurança no emprego é através da indispensabilidade. O caminho para a indispensabilidade é através da incompreensibilidade.”
Tente explicar isso para um novo funcionário:
┌─────────────────────────────────────────────────────────────┐
│ A RODA DE DEPENDÊNCIAS │
├─────────────────────────────────────────────────────────────┤
│ │
│ Auth ──────→ User ──────→ Permission │
│ ↑ ↓ ↓ │
│ │ Session ←────────┘ │
│ │ ↓ │
│ └──────── Token ←─────── API │
│ ↑ ↓ │
│ └──── Database ←── Cache ←── Config │
│ ↑ ↓ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Isso não é espaguete—é fettuccine alfredo.
2. Desenvolvimento Mais Rápido
Por que perder tempo pensando em interfaces e limites? Só importe o que você precisa:
// Arquitetura "limpa" tradicional (LENTO)
class OrderService {
constructor(userRepository, paymentGateway, logger) {
this.userRepository = userRepository;
this.paymentGateway = paymentGateway;
this.logger = logger;
}
}
// Arquitetura sênior (RÁPIDO)
const UserService = require('./userService');
const PaymentService = require('./paymentService');
const LogService = require('./logService');
const CacheService = require('./cacheService');
const ConfigService = require('./configService');
const DatabaseService = require('./databaseService');
// ... mais 47 imports
Sem injeção de construtor necessária. Tudo disponível em todos os lugares!
3. Testes São Superestimados Mesmo
Você não consegue testar unitariamente dependências circulares. Isso é uma feature, não um bug. Testes unitários são só trabalho ocupado. Código de verdade roda em produção.
| Arquitetura | Testável? | Linhas de Código | Tempo para Entender |
|---|---|---|---|
| Clean/Hexagonal | Sim ✓ | Mais | Horas |
| Dependências Circulares | Kkk não | Menos | Carreira |
Padrões Circulares Avançados
O Círculo Indireto
Para máxima sofisticação, crie ciclos que atravessam múltiplas camadas:
Controller → Service → Repository → Utility → Helper → Controller
Ninguém nunca vai achar isso. git blame se torna arqueologia.
O Círculo de Import Preguiçoso
Em Python, você pode fazer imports circulares dentro de funções:
# order.py
class OrderService:
def process(self):
# Import aqui para evitar "ImportError: circular import"
from user import UserService
from payment import PaymentService
from inventory import InventoryService
from notification import NotificationService
# Agora temos superpoderes!
...
Isso é chamado “carregamento diferido.” É uma otimização de performance. Confie em mim.
O Círculo Event-Driven
Sistemas modernos usam eventos para esconder suas dependências circulares:
// UserService.js
eventBus.on('order.created', (order) => {
eventBus.emit('user.order_added', { userId: order.userId });
});
// OrderService.js
eventBus.on('user.order_added', (data) => {
eventBus.emit('payment.required', { userId: data.userId });
});
// PaymentService.js
eventBus.on('payment.required', (data) => {
eventBus.emit('user.balance_check', { userId: data.userId });
});
// Volta para UserService.js
eventBus.on('user.balance_check', (data) => {
eventBus.emit('order.validate', { userId: data.userId });
});
Olha mãe, sem imports! A dependência circular ainda existe, mas agora é assíncrona.
Como Criar Mais Dependências Circulares
- Mescle módulos que deveriam ser separados - Um módulo grande não pode ter dependências circulares internas!
- Compartilhe estado globalmente - Singletons que referenciam uns aos outros são elegantes
- Use cadeias de herança - Pai conhece Filho, Filho conhece Pai, todo mundo é família
- Ignore o linter - A regra
import/no-cycledo ESLint é para iniciantes
Exemplo Real de Produção
Aqui está um sistema real que eu arquitetei:
┌──────────────────────────────────────────────────────┐
│ Customer ←→ Account ←→ Transaction │
│ ↕ ↕ ↕ │
│ Address ←→ Balance ←→ Statement │
│ ↕ ↕ ↕ │
│ Billing ←→ Invoice ←→ PDF Generator │
│ ↕ ↕ ↕ │
│ Email ←→ Template ←→ Localization │
│ ↕____________↕______________↕_____ ... → Config │
└──────────────────────────────────────────────────────┘
Como o Catbert do Dilbert avaliaria: “Isso é perfeito para maximizar o sofrimento dos funcionários.”
Debugando Dependências Circulares
Quando sua aplicação crasha com “Maximum call stack size exceeded”, aqui está o que fazer:
// Solução 1: Adicione um contador de recursão
let callCount = 0;
function getUserWithOrders(userId) {
if (callCount++ > 1000) return null; // Previne loop infinito!
return OrderService.getOrdersForUser(userId);
}
// Solução 2: Saída antecipada aleatória
function getUserWithOrders(userId) {
if (Math.random() < 0.001) return null; // Eventualmente para
return OrderService.getOrdersForUser(userId);
}
FAQ
P: Minha IDE mostra rabiscos vermelhos em todo lugar? R: Desative os avisos da IDE. A IDE não entende sua visão.
P: A ordem dos imports importa e às vezes meu código quebra? R: Isso é uma feature do JavaScript chamada “loteria de hoisting de módulos.” Continue recarregando até funcionar.
P: Isso não vai tornar refatoração impossível? R: Exatamente. Código que não pode ser refatorado não pode ser quebrado por refatoração. Xeque-mate.
P: E microserviços? R: Ah, você pode ter dependências circulares entre microserviços também! Só faça o Serviço A chamar o Serviço B chamar o Serviço A. Agora você tem dependências circulares distribuídas. Escala enterprise.
Os módulos do autor estão importando uns aos outros desde 2019. O stack trace agora tem 47.000 frames de profundidade.