Capa do artigo: Low Coupling e High Cohesion: A Dança das Dependências
Série Boas práticas com GRASP · Parte 2

Low Coupling e High Cohesion: A Dança das Dependências

Série Boas práticas com GRASP Parte 2

Reconectando com as Colmeias

No artigo anterior, exploramos Information Expert e Creator, os princípios que determinam onde colocar conhecimento e quem deve criar objetos. Vimos como nossa colmeia urbana sabe analisar seus próprios padrões de vibração e como ela naturalmente cria alertas quando detecta riscos. As responsabilidades encontraram seus donos naturais. Agora o desafio é fazer essas responsabilidades colaborarem sem se tornarem dependentes umas das outras.

Mas responsabilidades isoladas não formam sistemas. Uma colmeia não funciona porque cada abelha é expert em sua tarefa individual. Funciona porque existe uma organização emergente, uma rede de dependências mínimas mas suficientes. As abelhas operárias coletam pólen sem precisar saber detalhes da produção de mel. As abelhas nutridoras alimentam larvas sem precisar conhecer rotas de voo. Cada uma faz seu trabalho, conectando-se com outras apenas no necessário.

No código, essa é a essência de Low Coupling e High Cohesion: responsabilidades bem definidas que colaboram através de interfaces mínimas, onde cada componente é fortemente coeso internamente mas fracamente acoplado externamente.

Low Coupling: A Arte de Não Se Amarrar

Low Coupling é talvez o princípio mais pregado e menos compreendido. Todos sabem que acoplamento é ruim, mas poucos entendem que acoplamento zero é impossível e, em alguns casos, indesejável. O objetivo não é eliminar acoplamento, mas controlá-lo e direcioná-lo para os lugares certos.

A Ilusão do Acoplamento Zero e a Realidade da Metafísica

Existe uma tendência no desenvolvimento de software de buscar abstrações cada vez mais altas, camadas sobre camadas de indireção, tudo em nome da "redução de acoplamento". Essa busca pelo acoplamento zero me lembra da jornada filosófica de Sócrates e Platão em direção à metafísica.

Platão propôs que o mundo físico que percebemos é apenas uma sombra imperfeita de um mundo de formas ideais, abstratas e perfeitas. Uma cadeira específica que você vê é uma instância imperfeita da Forma ideal de "cadeirice". Essa abstração crescente levou à metafísica: o estudo de realidades além do físico, cada vez mais distante do concreto e tangível.

No código, vejo desenvolvedores criando camadas de abstração similares. Uma classe concreta é "impura", então criamos uma interface. Mas a interface ainda está "acoplada" a um domínio específico, então criamos uma interface mais genérica. E continuamos subindo em direção a abstrações platônicas: IEntity, IService, IProcessor, IHandler. Eventualmente, chegamos a abstrações tão genéricas que perderam todo significado concreto.

O problema é o mesmo que filósofos posteriores identificaram na metafísica platônica: quando você se afasta demais do concreto em busca do abstrato puro, você perde a capacidade de falar sobre coisas reais. Um IProcessor pode ser qualquer coisa que processa algo. Mas o que ele processa? Como? Por quê? A abstração, levada ao extremo, torna o código incompreensível para quem não tem profundidade para navegar essas camadas.

// ABSTRAÇÃO EXCESSIVA - metafísica no código
interface IProcessor<TInput, TOutput, TContext> {
    process(input: TInput, context: TContext): Promise<TOutput>;
}

interface IHandler<TRequest, TResponse> {
    handle(request: TRequest): Promise<TResponse>;
}

interface IExecutor<TCommand, TResult> {
    execute(command: TCommand): Promise<TResult>;
}

// Três interfaces que são essencialmente a mesma coisa
// mas tão abstratas que não comunicam nada sobre o domínio

Compare com abstrações que mantêm significado de domínio:

// ABSTRAÇÃO COM SIGNIFICADO DE DOMÍNIO
interface HiveHealthAssessor {
    assess(hive: BeeHive): Promise<HiveHealthReport>;
}

interface SwarmRiskDetector {
    detectRisk(vibrationData: VibrationMeasurement[]): SwarmRiskLevel;
}

interface AlertNotifier {
    notify(recipient: Beekeeper, alert: Alert): Promise<void>;
}

// Cada interface expressa um conceito específico do domínio
// mantendo flexibilidade sem perder significado

A lição aqui é crucial: Low Coupling não significa abstrair tudo até perder significado. Significa desacoplar do que é volátil e instável, mas permanecer acoplado ao que é estável e essencial. O domínio é estável. As regras de negócio são essenciais. Infraestrutura, frameworks, vendors são voláteis.

Aristóteles, aluno de Platão, criticou a teoria das formas justamente por esse problema. Ele argumentou que devemos estudar as coisas concretas e reais, não apenas suas abstrações ideais. No código, isso se traduz em manter as abstrações próximas ao domínio concreto. Não crie IProcessor genérico, crie HiveHealthAssessor específico.

Acoplamento Inerente vs Acoplamento Acidental

Nem todo acoplamento é igual. Existe acoplamento inerente (essencial, inevitável, desejável) e acoplamento acidental (desnecessário, problemático).

Acoplamento inerente acontece quando duas coisas estão fundamentalmente relacionadas no domínio. Uma colmeia está inerentemente acoplada à sua localização geográfica. Não faz sentido desacoplar isso com uma camada de abstração gigantesca. A colmeia precisa de localização para existir.

class UrbanBeeHive {
    constructor(
        private readonly id: HiveId,
        private readonly location: RooftopLocation  // Acoplamento inerente e correto
    ) {}

    // A colmeia DEVE saber sua localização
    isInCriticalZone(): boolean {
        return this.location.isInUrbanCenter();
    }
}

Acoplamento acidental acontece quando uma dependência existe por escolhas de implementação, não por necessidade do domínio. Uma colmeia não deve saber sobre o formato de serialização JSON, ou qual banco de dados persiste seus dados, ou qual API de mapas representa sua localização.

// ACOPLAMENTO ACIDENTAL - EVITE
class UrbanBeeHive {
    constructor(
        private readonly googleMapsAPI: GoogleMapsAPI  // Acidental!
    ) {}

    getLocationCoordinates(): {lat: number, lng: number} {
        // Colmeia não deveria conhecer Google Maps API
        return this.googleMapsAPI.geocode(this.address);
    }
}

// CORRETO - acoplamento ao conceito, não à implementação
class UrbanBeeHive {
    constructor(
        private readonly location: RooftopLocation  // Conceito de domínio
    ) {}

    getCoordinates(): GeographicCoordinates {
        return this.location.coordinates;
    }
}

Low Coupling significa eliminar acoplamento acidental enquanto aceita acoplamento inerente. Essa distinção requer julgamento e conhecimento do domínio. Não há regra mecânica que substitui entendimento profundo.

Múltiplos Caminhos para Baixo Acoplamento

Eventos de domínio são uma das abordagens para desacoplar a colmeia de serviços de notificação, e vão aparecer nos exemplos adiante. Mas não são a única solução. Existem múltiplos padrões para alcançar baixo acoplamento, cada um adequado a contextos diferentes.

Observer Pattern: Observadores Diretos

O padrão Observer estabelece uma relação um-para-muitos onde um sujeito notifica múltiplos observadores quando seu estado muda.

interface HiveStateObserver {
    onHiveStateChanged(hive: BeeHive, change: StateChange): void;
}

class BeeHive {
    private observers: HiveStateObserver[] = [];

    subscribe(observer: HiveStateObserver): void {
        this.observers.push(observer);
    }

    unsubscribe(observer: HiveStateObserver): void {
        this.observers = this.observers.filter(o => o !== observer);
    }

    private notifyStateChange(change: StateChange): void {
        this.observers.forEach(o => o.onHiveStateChanged(this, change));
    }

    updateTemperature(temp: Temperature): void {
        const oldTemp = this.temperature;
        this.temperature = temp;
        this.notifyStateChange(new TemperatureChanged(oldTemp, temp));
    }
}

class TemperatureMonitor implements HiveStateObserver {
    onHiveStateChanged(hive: BeeHive, change: StateChange): void {
        if (change instanceof TemperatureChanged) {
            if (change.newTemp.isCritical()) {
                this.alertService.sendCriticalTemperatureAlert(hive.id, change.newTemp);
            }
        }
    }
}

Observer é direto e simples, mas cria um acoplamento sutil: o sujeito (BeeHive) mantém referências aos observadores, o que pode causar problemas de lifecycle e memory leaks se não gerenciado cuidadosamente.

Event Bus: Mediação Completa

Um Event Bus centraliza a publicação e subscrição de eventos, removendo completamente a necessidade do publisher conhecer subscribers.

class DomainEventBus {
    private handlers = new Map<string, Array<(event: any) => void>>();

    subscribe<T extends DomainEvent>(
        eventType: string,
        handler: (event: T) => void
    ): void {
        if (!this.handlers.has(eventType)) {
            this.handlers.set(eventType, []);
        }
        this.handlers.get(eventType)!.push(handler);
    }

    publish(event: DomainEvent): void {
        const handlers = this.handlers.get(event.type) || [];
        handlers.forEach(handler => handler(event));
    }
}

class BeeHive {
    constructor(
        private readonly eventBus: DomainEventBus
    ) {}

    updateTemperature(temp: Temperature): void {
        const oldTemp = this.temperature;
        this.temperature = temp;

        // Publica sem conhecer quem vai consumir
        this.eventBus.publish(new TemperatureChangedEvent(
            this.id,
            oldTemp,
            temp
        ));
    }
}

// Em algum lugar da camada de aplicação
eventBus.subscribe('TemperatureChanged', (event: TemperatureChangedEvent) => {
    if (event.newTemp.isCritical()) {
        alertService.sendCriticalTemperatureAlert(event.hiveId, event.newTemp);
    }
});

Event Bus desacopla completamente publisher de subscribers, mas adiciona indireção e pode tornar o fluxo de controle difícil de rastrear. É poderoso para sistemas onde eventos realmente precisam cruzar boundaries de módulos ou contextos.

Dependency Injection: Invertendo o Controle

Dependency Injection inverte quem controla as dependências: em vez de uma classe instanciar suas dependências, elas são injetadas de fora.

// SEM DI - alto acoplamento
class HiveMonitoringService {
    private readonly repository = new PostgresHiveRepository();  // Acoplado!
    private readonly notifier = new SendGridEmailNotifier();    // Acoplado!

    async checkHive(hiveId: HiveId): Promise<void> {
        const hive = await this.repository.findById(hiveId);
        // ...
    }
}

// COM DI - baixo acoplamento
class HiveMonitoringService {
    constructor(
        private readonly repository: HiveRepository,      // Interface!
        private readonly notifier: AlertNotifier          // Interface!
    ) {}

    async checkHive(hiveId: HiveId): Promise<void> {
        const hive = await this.repository.findById(hiveId);
        // ...
    }
}

// Composição acontece no composition root
const service = new HiveMonitoringService(
    new PostgresHiveRepository(dbConnection),
    new MultiChannelNotifier(emailService, smsService)
);

DI é fundamental para testabilidade e para aplicar Dependency Inversion Principle (DIP). Permite trocar implementações sem tocar no código que as usa.

Strategy Pattern: Comportamentos Intercambiáveis

Strategy encapsula algoritmos intercambiáveis, permitindo que o cliente escolha qual usar sem conhecer detalhes de implementação.

interface SwarmDetectionStrategy {
    detectRisk(pattern: VibrationPattern): SwarmRiskLevel;
}

class FrequencyThresholdStrategy implements SwarmDetectionStrategy {
    detectRisk(pattern: VibrationPattern): SwarmRiskLevel {
        // Algoritmo baseado em threshold simples
    }
}

class MachineLearningStrategy implements SwarmDetectionStrategy {
    detectRisk(pattern: VibrationPattern): SwarmRiskLevel {
        // Algoritmo baseado em ML
    }
}

class SwarmMonitor {
    constructor(
        private readonly strategy: SwarmDetectionStrategy  // Desacoplado do algoritmo específico
    ) {}

    assessSwarmRisk(hive: BeeHive): SwarmRiskLevel {
        const pattern = hive.getVibrationPattern();
        return this.strategy.detectRisk(pattern);  // Delega para a estratégia
    }
}

// Flexibilidade para trocar estratégias
const basicMonitor = new SwarmMonitor(new FrequencyThresholdStrategy());
const advancedMonitor = new SwarmMonitor(new MachineLearningStrategy());

Strategy é Low Coupling através de polimorfismo. O monitor não está acoplado a um algoritmo específico, está acoplado ao conceito abstrato de "estratégia de detecção".

Observer funciona bem quando o sujeito precisa notificar observadores no mesmo processo e o ciclo de vida deles é controlado. Event Bus é mais adequado quando eventos precisam cruzar boundaries de módulos ou quando publisher e subscriber não devem se conhecer de forma alguma. DI resolve o problema de dependências de infraestrutura e é indispensável para testabilidade. Strategy encapsula variação de comportamento quando o algoritmo precisa ser intercambiável sem alterar quem o usa. Esses padrões não se excluem. Em um sistema real, os quatro coexistem em camadas diferentes. A escolha de cada um depende da natureza do acoplamento que precisa ser reduzido.

A Sabedoria do Acoplamento Consciente

A arte de Low Coupling não está em eliminar toda dependência, mas em tornar-se consciente de cada uma. A primeira pergunta diante de qualquer dependência é se ela é inerente ou acidental. Acoplamento inerente deve ser aceito. Acoplamento acidental deve ser removido.

A segunda pergunta é se a dependência aponta para algo estável ou algo volátil. Depender de uma implementação que muda por razões externas ao domínio é uma fonte garantida de instabilidade. A resposta é criar indireção por meio de abstrações estáveis, tipicamente interfaces que expressam conceitos do domínio em vez de detalhes técnicos. Quando uma dependência atravessa fronteiras arquiteturais, essa proteção deixa de ser opcional.

Uma forma prática de detectar acoplamento excessivo é verificar se o código pode ser testado sem aquela dependência. Se não puder, Dependency Injection resolve o problema invertendo o controle. O teste não é apenas uma rede de segurança, é um detector de acoplamento.

"Garanta que os módulos que estão fazendo a lógica de negócio da sua aplicação não estejam acoplados ao mundo externo."
— Robert C. Martin, Clean Architecture

Low Coupling é equilíbrio. É sabedoria arquitetural, não aplicação mecânica de regras. É Aristóteles, não Platão. Fundamentado no concreto, consciente das abstrações necessárias.

High Cohesion: Quando Tudo se Conecta no Lugar Certo

Se Low Coupling governa como componentes se relacionam com o mundo externo, High Cohesion governa o que pode coexistir dentro de um componente. Uma classe coesa é aquela onde tudo que está dentro tem uma razão relacionada de existir junto, onde cada método, cada atributo, cada decisão aponta para o mesmo propósito.

No processamento de dados de sensores, a tentação é criar uma classe DataProcessor que processa vibração, temperatura, umidade e peso, e ainda faz validação de dados malformados e conversão de unidades. Tecnicamente funciona, mas a classe tem baixa coesão. Os elementos estão juntos apenas pelo fato de serem processamento de dados, não por compartilharem um propósito real.

// BAIXA COESÃO - evite
class SensorDataProcessor {
    processVibration(raw: number): VibrationMeasurement { /* ... */ }
    processTemperature(raw: number): Temperature { /* ... */ }
    processHumidity(raw: number): Humidity { /* ... */ }
    processWeight(raw: number): Weight { /* ... */ }
    validateDataFormat(raw: string): boolean { /* ... */ }
    convertCelsiusToFahrenheit(c: number): number { /* ... */ }
    convertKilogramsToPounds(kg: number): number { /* ... */ }
    detectAnomalies(data: any[]): Anomaly[] { /* ... */ }
}

Essa classe tem múltiplas razões para mudar: mudanças em como processar vibração, temperatura, formato de validação, unidades de medida, detecção de anomalias. Isso é baixa coesão e viola SRP simultaneamente.

High Cohesion sugere separar por propósito coeso:

class VibrationDataProcessor {
    private readonly calibration: SensorCalibration;

    process(raw: RawVibrationData): VibrationMeasurement {
        const calibrated = this.applyCalibration(raw);
        const validated = this.ensureWithinPhysicalBounds(calibrated);
        return VibrationMeasurement.fromHertz(validated.frequency);
    }

    private applyCalibration(raw: RawVibrationData): CalibratedData {
        return this.calibration.apply(raw);
    }

    private ensureWithinPhysicalBounds(data: CalibratedData): ValidatedData {
        if (data.frequency < 0 || data.frequency > 2000) {
            throw new SensorDataOutOfBoundsError(data);
        }
        return ValidatedData.from(data);
    }
}

class TemperatureDataProcessor {
    private readonly calibration: SensorCalibration;
    private readonly unit: TemperatureUnit;

    process(raw: RawTemperatureData): Temperature {
        const calibrated = this.applyCalibration(raw);
        const validated = this.ensureWithinPhysicalBounds(calibrated);
        return Temperature.create(validated.degrees, this.unit);
    }

    // Métodos relacionados apenas ao processamento de temperatura
}

Agora cada processador tem alta coesão: tudo dentro dele é sobre processar um tipo específico de dado sensorial. As razões para mudança estão relacionadas. Isso também segue KISS (Keep It Simple, Stupid): cada classe faz uma coisa de forma simples e compreensível.

DRY vs Coesão: Quando Duplicar é Mais Honesto

A provocação aqui é que muitas vezes criamos abstrações prematuras tentando reutilizar código entre processadores diferentes, quando na verdade eles apenas compartilham uma forma similar, mas não uma essência comum. DRY (Don't Repeat Yourself) não significa eliminar toda duplicação visual, significa eliminar duplicação de conhecimento. Se o processamento de vibração e temperatura parecem similares em forma mas são diferentes em essência, duplicar é mais honesto que abstrair forçadamente.

// ABSTRAÇÃO FORÇADA - baixa coesão por tentar evitar duplicação
class GenericSensorProcessor<T> {
    process(raw: number, converter: (n: number) => T): T {
        const validated = this.validate(raw);
        return converter(validated);
    }

    private validate(raw: number): number {
        // Validação genérica que na verdade não se aplica a todos os sensores
    }
}

// USO confuso que não expressa domínio
const vibration = genericProcessor.process(rawData, (n) => VibrationMeasurement.fromHertz(n));
// DUPLICAÇÃO HONESTA - alta coesão em cada processador
class VibrationDataProcessor {
    process(raw: RawVibrationData): VibrationMeasurement {
        this.validateVibrationRange(raw);  // Validação específica
        return VibrationMeasurement.fromHertz(raw.frequency);
    }

    private validateVibrationRange(raw: RawVibrationData): void {
        if (raw.frequency < 0 || raw.frequency > 2000) {
            throw new InvalidVibrationDataError(raw);
        }
    }
}

class TemperatureDataProcessor {
    process(raw: RawTemperatureData): Temperature {
        this.validateTemperatureRange(raw);  // Validação específica e diferente
        return Temperature.celsius(raw.degrees);
    }

    private validateTemperatureRange(raw: RawTemperatureData): void {
        if (raw.degrees < -273.15) {  // Zero absoluto
            throw new PhysicallyImpossibleTemperatureError(raw);
        }
    }
}

Sim, há uma estrutura similar em forma. Processo, validação, conversão. Mas o que define DRY não é a forma, é o conhecimento. DRY diz que cada pedaço de conhecimento deve ter uma representação única no sistema. O conhecimento sobre limites físicos de temperatura é diferente do conhecimento sobre limites operacionais de um sensor de vibração. Forçar os dois dentro de uma abstração genérica não elimina duplicação de conhecimento. Cria uma abstração que não pertence a nenhum dos dois domínios, enfraquecendo a coesão de ambos.

A honestidade aqui é uma virtude de engenharia. Quando dois conceitos são fundamentalmente distintos, mantê-los separados é mais preciso do que compactá-los artificialmente. A duplicação visual desaparece, mas a confusão conceitual permanece, agora invisível por trás de um genérico.

Coesão em Escala: Pacotes e Módulos

High Cohesion não se limita a classes individuais. Aplica-se também a pacotes, módulos, e componentes maiores. Um pacote coeso agrupa classes que mudam pelas mesmas razões e colaboram para um propósito comum.

Considere a organização de código em pacotes para nosso sistema de colmeias:

src/
├── domain/
│   ├── hive/
│   │   ├── BeeHive.ts
│   │   ├── HiveId.ts
│   │   ├── HiveHealthScore.ts
│   │   └── SwarmVibrationPattern.ts
│   ├── alert/
│   │   ├── Alert.ts
│   │   ├── AlertSeverity.ts
│   │   └── AlertType.ts
│   └── sensor/
│       ├── VibrationMeasurement.ts
│       ├── Temperature.ts
│       └── Humidity.ts
├── application/
│   ├── AssessHiveHealthUseCase.ts
│   └── ProcessSensorDataUseCase.ts
└── infrastructure/
    ├── persistence/
    │   └── PostgresHiveRepository.ts
    └── notification/
        └── MultiChannelNotifier.ts

Cada pacote tem alta coesão interna: domain/hive/ contém apenas conceitos relacionados à colmeia, domain/alert/ apenas conceitos de alertas. Eles colaboram, mas cada um tem uma razão específica para existir e mudar.

Acoplamento Aferente e Eferente

Quando falamos de coesão e acoplamento em escala de pacotes, dois conceitos se tornam cruciais: acoplamento aferente (Ca) e acoplamento eferente (Ce).

Acoplamento Eferente (Ce): número de classes fora do pacote que classes dentro do pacote dependem. Mede quanto o pacote depende de outros. Alto Ce significa que mudanças em outros pacotes afetam este.

Acoplamento Aferente (Ca): número de classes fora do pacote que dependem de classes dentro dele. Mede quanto outros dependem deste pacote. Alto Ca significa que mudanças neste pacote afetam muitos outros.

// Pacote domain/hive/
// Ca (aferente): application, infrastructure dependem dele (Ca alto)
// Ce (eferente): depende apenas de domain/sensor (Ce baixo)

// Pacote application/
// Ca: apenas infrastructure depende dele (Ca médio)
// Ce: depende de domain/hive, domain/alert (Ce médio)

// Pacote infrastructure/
// Ca: ninguém depende (Ca zero ou muito baixo)
// Ce: depende de domain e application (Ce alto)

A métrica útil aqui é Instabilidade = Ce / (Ca + Ce). Varia de 0 (máxima estabilidade, todos dependem, você não depende de ninguém) a 1 (máxima instabilidade, você depende de todos, ninguém depende de você).

Princípio da Dependência Estável: dependências devem apontar na direção da estabilidade. Pacotes instáveis devem depender de pacotes estáveis, não o contrário.

No nosso caso:

  • domain/: Instabilidade ~0.1 (muito estável, todos dependem dele, ele depende de quase nada)
  • application/: Instabilidade ~0.5 (moderadamente estável)
  • infrastructure/: Instabilidade ~0.9 (altamente instável, não é problema, ele é a borda)

Essa é a direção correta: infrastructure → application → domain. Nunca o contrário.

Dependências Cíclicas: O Veneno da Coesão

Uma das piores violações de coesão e acoplamento são dependências cíclicas entre pacotes. É quando pacote A depende de B, que depende de C, que depende de A. Forma um ciclo que torna impossível entender, testar ou modificar qualquer parte isoladamente.

// CICLO PROBLEMÁTICO

// domain/hive/BeeHive.ts
import { AlertService } from '../../application/AlertService';

class BeeHive {
    constructor(private alertService: AlertService) {}  // Domínio dependendo de aplicação!
}

// application/AlertService.ts
import { BeeHive } from '../domain/hive/BeeHive';

class AlertService {
    checkHive(hive: BeeHive): void {  // Aplicação dependendo de domínio (ok)
        // Mas agora temos um ciclo: BeeHive → AlertService → BeeHive
    }
}

Este ciclo é venenoso. Para compilar BeeHive, preciso de AlertService. Para compilar AlertService, preciso de BeeHive. Eles estão acoplados ciclicamente, efetivamente tornando-se um único componente inseparável.

A solução é quebrar o ciclo com inversão de dependência:

// domain/hive/BeeHive.ts
import { DomainEventPublisher } from '../events/DomainEventPublisher';

class BeeHive {
    constructor(private eventPublisher: DomainEventPublisher) {}

    assessCurrentState(): void {
        // ...
        if (risk) {
            // Publica evento, não chama service diretamente
            this.eventPublisher.publish(new SwarmRiskDetected(this.id, risk));
        }
    }
}

// application/AlertService.ts
import { BeeHive } from '../domain/hive/BeeHive';
import { SwarmRiskDetected } from '../domain/events/SwarmRiskDetected';

class AlertService {
    constructor(eventPublisher: DomainEventPublisher) {
        // Se inscreve no evento
        eventPublisher.subscribe('SwarmRiskDetected', (event) => {
            this.handleSwarmRisk(event);
        });
    }

    private handleSwarmRisk(event: SwarmRiskDetected): void {
        // Lida com o alerta sem BeeHive depender de AlertService
    }
}

Agora: BeeHive → DomainEventPublisher ← AlertService. Não há ciclo. BeeHive não conhece AlertService, ambos conhecem apenas o mecanismo de eventos.

Ferramentas de análise estática de dependências detectam ciclos automaticamente. madge cumpre esse papel para projetos TypeScript e JavaScript. Para Java, o SonarQube identifica dependências cíclicas entre pacotes e gera relatórios de acoplamento que podem ser integrados ao pipeline de CI. Dependências cíclicas são uma das principais fontes de complexidade acidental e precisam ser eliminadas antes de se tornarem estruturais.

Se quiser aprofundar nesse tema, há um artigo dedicado ao Princípio das Dependências Acíclicas, cobrindo desde a teoria de grafos até o impacto no custo da mudança em sistemas que cresceram além do monólito.

A Métrica de Coesão: LCOM

Coesão também pode ser medida. Existe uma métrica formal chamada LCOM (Lack of Cohesion of Methods) que quantifica o que a intuição já suspeita. Ela mede quantos grupos distintos de métodos existem em uma classe, onde cada grupo acessa um conjunto diferente de atributos. Uma colmeia onde metade das abelhas cuida de temperatura e a outra metade gerencia cobranças não é uma colmeia coesa. LCOM alto é o indicador numérico disso no código.

Uma classe com LCOM alto (muitos grupos) tem baixa coesão: métodos não colaboram, apenas coexistem na mesma classe sem razão real. Uma classe com LCOM baixo tem alta coesão: métodos trabalham com os mesmos dados em colaboração.

// LCOM ALTO - baixa coesão
class HiveManager {
    // Grupo 1 de atributos
    private temperature: Temperature;
    private humidity: Humidity;

    // Grupo 2 de atributos (não relacionado ao grupo 1)
    private ownerId: string;
    private billingInfo: BillingInfo;

    // Métodos do grupo 1
    assessEnvironment(): void {
        // Usa temperature e humidity
    }

    // Métodos do grupo 2
    chargeMaintenance(): void {
        // Usa ownerId e billingInfo
    }

    // LCOM alto: dois grupos distintos que não deveriam estar juntos
}

// SEPARAR em classes coesas
class HiveEnvironment {
    private temperature: Temperature;
    private humidity: Humidity;

    assess(): EnvironmentAssessment {
        // Todos os métodos usam temperature e humidity
    }
}

class HiveOwnership {
    private ownerId: string;
    private billingInfo: BillingInfo;

    chargeMaintenance(): void {
        // Todos os métodos usam ownerId e billingInfo
    }
}

LCOM é uma métrica útil para detectar classes que estão fazendo demais e precisam ser divididas para aumentar coesão.

A Dança de Low Coupling e High Cohesion

Low Coupling e High Cohesion são forças complementares que atuam em direções opostas e precisam de equilíbrio. Alta coesão dentro de um componente significa que tudo que está junto muda pelas mesmas razões. Baixo acoplamento entre componentes significa que mudanças em um não propagam pressão para os outros. O ponto de tensão aparece quando separar demais fragmenta o que deveria estar unido, e unir demais cria o God Object que tudo sabe e tudo faz.

A colmeia resolve essa tensão com precisão biológica. Cada abelha tem um papel coeso, especializado, inconfundível. Mas colabora por meio de protocolos mínimos e suficientes. Não há uma abelha que faz tudo, e não há abelhas sem comunicação com o coletivo. O sistema funciona porque cada unidade é coesa internamente e fracamente acoplada externamente.

No código, esse equilíbrio não é um estado que se atinge uma vez. É uma pressão constante, um julgamento que se refaz a cada nova dependência, a cada nova responsabilidade que encontra um lugar onde morar.

"A software system is well-structured if its modules are strongly cohesive and loosely coupled."
— Grady Booch, Object-Oriented Analysis and Design


O que Vem a Seguir

Agora que entendemos como organizar responsabilidades (Information Expert, Creator) e como conectá-las estrategicamente (Low Coupling, High Cohesion), surge uma questão prática crucial: quem coordena tudo isso? Quando múltiplas entidades precisam colaborar para realizar um caso de uso completo, quem é o maestro?

No próximo artigo, exploraremos Controller e Polymorphism. Veremos que Controller no GRASP é diferente do Controller do MVC, e entenderemos quando cada conceito se aplica. Descobriremos como polimorfismo não é apenas sobre herança, mas sobre atribuir responsabilidades baseadas em variação de comportamento. E enfrentaremos a provocação profissional: escrever mais código estruturado não é trabalho desperdiçado, é investimento em manutenibilidade.

A jornada continua. As abelhas não trabalham ao acaso, há uma coordenação implícita. No código, essa coordenação deve ser explícita, controlada, e adaptável às mudanças.