Arquitetura e Desenvolvimento

Coesão, Acoplamento e Responsabilidades no Design de Software

Dividir um sistema em classes e módulos é só o começo. O que faz um design ser fácil de manter é a forma como as responsabilidades são distribuídas e o quanto as partes dependem umas das outras.

Profissional trabalhando com desenvolvimento e estrutura de software.

Introdução

Quando alguém começa a programar, é comum pensar que organizar o código significa apenas quebrá-lo em arquivos e classes. Um arquivo para o cadastro, outro para os relatórios, e pronto. Só que dois sistemas com o mesmo número de classes podem ser muito diferentes na hora de mudar alguma coisa: em um deles, a alteração fica em um lugar só; no outro, ela se espalha por meio projeto.

A diferença está em duas perguntas. A primeira é o que cada parte deve fazer, ou seja, qual é a sua responsabilidade. A segunda é o quanto cada parte depende das outras. Três ideias ajudam a responder essas perguntas: responsabilidade, coesão e acoplamento.

Este artigo faz a ponte entre a modelagem e a arquitetura de software, com exemplos bons e ruins, diagramas, código em Python e um checklist.

O que são responsabilidades no design de software

Em linguagem simples, a responsabilidade de uma classe ou de um módulo é aquilo que ele deve saber ou fazer dentro do sistema. É a resposta para a pergunta “por que essa parte existe?”.

Costuma-se dividir as responsabilidades em dois tipos:

  • Responsabilidade de informação (saber): guardar e fornecer dados que fazem sentido para aquela parte. Uma classe Membro sabe o nome, o telefone e o e-mail de um membro.
  • Responsabilidade de comportamento (fazer): executar ações, tomar decisões ou coordenar outras partes. Um serviço de cadastro sabe validar os dados e decidir se um novo membro pode ser registrado.

Imagine um sistema de cadastro de membros usado como exemplo educacional. Ele precisa validar dados, salvar registros, gerar uma ficha em PDF e avisar a secretaria quando alguém novo se cadastra. Todas essas tarefas são necessárias, mas não precisam morar no mesmo lugar. Se uma única classe fizer tudo, qualquer mudança, seja no layout do PDF, na regra de validação ou na forma de enviar o aviso, vai exigir mexer justamente nessa classe.

Por isso, a primeira decisão de design é distribuir responsabilidades. Uma boa pista vem dos requisitos: cada necessidade descrita costuma apontar para uma responsabilidade, e necessidades muito diferentes raramente deveriam cair na mesma classe. Requisitos de qualidade, como desempenho e segurança, também influenciam essa divisão, como mostra o artigo sobre requisitos funcionais e não funcionais.

O que é coesão

Pense em uma caixa de ferramentas. Se ela guarda só chaves de fenda e chaves de boca, você sabe o que vai encontrar ao abri-la. Se ela guarda chaves, talheres, remédios e documentos, cada vez que você procura algo precisa revirar tudo. A primeira caixa é coesa; a segunda, não.

No software, coesão mede o quanto as responsabilidades de uma classe ou módulo estão relacionadas entre si. É uma medida “de dentro para dentro”: olha para o conteúdo de uma parte e pergunta se ele forma um conjunto com um propósito único.

Alta coesão

Uma classe tem alta coesão quando suas responsabilidades giram em torno de um mesmo assunto. Um RelatorioService que monta, filtra e formata relatórios é coeso: todos os métodos ajudam a produzir relatórios. Classes assim são fáceis de nomear, de entender e de testar, porque fazem uma coisa bem definida.

Baixa coesão

Uma classe tem baixa coesão quando reúne responsabilidades sem relação clara. Um sinal comum é o nome genérico: Sistema, Util, Gerenciador. Outro sinal é a dificuldade de descrever a classe em uma frase sem usar “e” várias vezes: “ela cadastra usuários e gera PDF e envia e-mail e calcula relatórios”.

Exemplo prático de coesão

Veja uma classe Sistema que concentra tudo: cadastrar usuário, gerar PDF, enviar e-mail, calcular relatório, acessar o banco de dados e enviar notificações. No começo, isso parece prático, porque tudo está em um só arquivo. Com o tempo, a classe cresce, cada mudança arrisca quebrar outra parte e ninguém tem coragem de mexer nela.

Exemplo de baixa coesãoUma classe central chamada Sistema está ligada a seis responsabilidades diferentes: Cadastro, PDF e E-mail à esquerda; Relatórios, Banco de Dados e Notificações à direita. O destaque em vermelho indica concentração excessiva de responsabilidades em uma única classe.Baixa coesão: tudo concentrado em uma classeCadastroPDFE-mailRelatóriosBanco de DadosNotificaçõesSistema6 responsabilidadessem relação clara⚠ difícil de mudarQualquer mudança em uma dessas áreas exige alterar a mesma classe.Exemplo educacional.
Legenda: a caixa vermelha no centro é a classe Sistema; as pílulas amarelas são responsabilidades de naturezas diferentes. As linhas tracejadas mostram que todas estão presas à mesma classe, o que caracteriza baixa coesão.

Uma divisão mais coerente separa as responsabilidades por assunto:

  • UsuarioService: regras do cadastro, como validar dados e impedir duplicidade;
  • UsuarioRepository: salvar e buscar usuários no armazenamento;
  • RelatorioService: montar e calcular relatórios, incluindo a geração do PDF;
  • EmailService: enviar e-mails, sem conhecer regras de cadastro.

Cada classe agora tem um nome que explica seu papel. Se o formato do e-mail mudar, a alteração fica no EmailService. Se a forma de armazenar dados mudar, ela fica no repositório. Repare que o objetivo não é criar muitas classes por criar: é agrupar o que muda pelos mesmos motivos e separar o que muda por motivos diferentes. Quatro classes coesas são melhores que uma classe gigante, mas vinte classes minúsculas sem propósito claro seriam um problema novo.

Comparação entre responsabilidade mal distribuída e responsabilidade bem distribuída
AspectoResponsabilidade mal distribuídaResponsabilidade bem distribuída
Mudança no e-mailExige mexer na classe que também cadastra e gera relatórios.Fica restrita ao EmailService.
TestesTestar o cadastro obriga a preparar banco, PDF e e-mail.Cada parte pode ser testada isoladamente.
Leitura do códigoÉ preciso entender a classe inteira para mudar um detalhe.O nome da classe indica onde procurar.
Trabalho em equipeVárias pessoas editam o mesmo arquivo ao mesmo tempo.Pessoas diferentes trabalham em partes diferentes.
ReusoNão dá para reaproveitar o envio de e-mail sem levar o resto.O EmailService pode ser usado por outras partes.

O que é acoplamento

Agora pense em aparelhos elétricos. Um abajur com plugue pode ser ligado em qualquer tomada padrão. Um abajur com os fios soldados direto na instalação da parede até funciona, mas, para trocá-lo de lugar, é preciso chamar um eletricista. Os dois dependem da energia; a diferença está em como dependem.

No software, acoplamento mede o grau de dependência entre componentes. É uma medida “de fora”: olha para as ligações entre as partes e pergunta o quanto uma precisa conhecer da outra para funcionar.

Alto acoplamento

Há alto acoplamento quando uma parte conhece detalhes internos de outra: cria diretamente uma implementação específica, chama métodos muito particulares dela ou depende da forma exata como ela guarda seus dados. O efeito prático é o efeito cascata: uma mudança em um componente obriga a alterar vários outros.

Baixo acoplamento

Há baixo acoplamento quando as partes se comunicam por meio de contratos simples e estáveis. Uma parte sabe o que a outra oferece, mas não como ela faz. Assim, é possível trocar ou modificar um componente com pouco impacto no restante.

Exemplo prático de acoplamento

No trecho abaixo, o UsuarioService cria por conta própria um enviador de e-mail específico e conhece detalhes do protocolo usado:

# Alto acoplamento: o serviço conhece a implementação concreta
class UsuarioService:
    def __init__(self):
        self.email = EnviadorSMTP()  # cria a dependência sozinho

    def cadastrar(self, usuario):
        # ... regras do cadastro ...
        self.email.conectar_servidor("smtp.exemplo.com", 587)
        self.email.enviar_smtp(usuario.email, "Bem-vindo!")

Se a organização decidir avisar os usuários por outro canal, ou se o servidor mudar, o serviço de cadastro precisa ser alterado, mesmo que nenhuma regra de cadastro tenha mudado. Testar o cadastro também fica difícil, porque todo teste tentaria se conectar a um servidor de e-mail.

Uma alternativa mais desacoplada é o serviço depender de uma abstração: qualquer objeto que saiba enviar uma mensagem.

# Baixo acoplamento: o serviço depende só de "algo que envia"
class UsuarioService:
    def __init__(self, notificador):
        self.notificador = notificador  # recebido de fora

    def cadastrar(self, usuario):
        # ... regras do cadastro ...
        self.notificador.enviar(usuario.email, "Bem-vindo!")

O contrato combinado é simples: o objeto recebido precisa ter um método enviar(destino, mensagem). Em Python, isso funciona naturalmente; em linguagens como Java ou C#, esse contrato costuma ser declarado como uma interface. Quem monta o sistema decide se o notificador envia e-mail, mensagem de celular ou apenas grava em um arquivo durante os testes. O serviço de cadastro continua igual.

Note que a dependência não desapareceu: o serviço ainda precisa de alguém que envie mensagens. Ela apenas ficou mais leve e mais fácil de trocar.

Coesão × acoplamento

Os dois conceitos costumam aparecer juntos, mas medem coisas diferentes:

Comparação entre coesão e acoplamento
ConceitoO que medeSituação desejávelProblema quando mal aplicadoExemplo
CoesãoO quanto as responsabilidades de uma parte estão relacionadas.Alta coesão.Classes “faz tudo”, difíceis de entender e de testar.RelatorioService cuida só de relatórios.
AcoplamentoO quanto uma parte depende de outras.Baixo acoplamento.Efeito cascata: uma mudança exige alterar várias partes.UsuarioService recebe um notificador em vez de criar um enviador específico.

A combinação alta coesão + baixo acoplamento é desejável porque os dois efeitos se reforçam. Quando cada parte cuida de um assunto, ela precisa de menos coisas das outras. E quando as dependências são poucas e simples, fica mais fácil manter cada parte focada.

Isso, porém, não significa eliminar toda dependência. Um sistema é feito de partes que colaboram; sem ligações, nada funciona. O objetivo é que as dependências existentes sejam necessárias, explícitas e estáveis, e que não haja ligações acidentais criadas só por conveniência.

Alta coesão e baixo acoplamentoTrês grupos separados. Módulo de usuários: UsuarioService depende de UsuarioRepository. Módulo de relatórios: RelatorioService depende de RelatorioRepository. Comunicação: EmailService e NotificacaoService, independentes entre si. Cada classe tem uma responsabilidade clara, e as dependências são poucas e diretas.Módulo de usuáriosUsuarioServiceregras do cadastrodepende deUsuarioRepositorysalvar e buscar usuáriosMódulo de relatóriosRelatorioServicemontar e calcular relatóriosdepende deRelatorioRepositoryconsultar dados do relatórioComunicaçãoEmailServiceenviar e-mailsNotificacaoServiceenviar notificações
Legenda: cada caixa verde é uma classe com uma responsabilidade clara; as áreas tracejadas agrupam classes do mesmo assunto; as setas indicam dependências diretas. Os serviços de comunicação podem ser usados por outros módulos quando necessário, mas essas ligações não foram desenhadas para manter o exemplo simples. Exemplo educacional.

Relação com os diagramas UML

Coesão e acoplamento não aparecem só no código. Eles podem ser percebidos, e corrigidos, ainda durante a modelagem, quando mudar custa pouco:

  • Diagrama de Classes: mostra as responsabilidades (atributos e métodos) e os relacionamentos. Uma classe com dezenas de métodos de assuntos diferentes sugere baixa coesão; uma classe ligada a quase todas as outras sugere alto acoplamento.
  • Diagrama de Sequência: mostra como os objetos colaboram. Se um único participante envia e recebe quase todas as mensagens, ele provavelmente concentra responsabilidades demais.
  • Diagrama de Atividades: mostra como as etapas de um processo são distribuídas. Raias bem definidas ajudam a perceber quem deveria cuidar de cada etapa.
  • Casos de Uso: mostram o que o sistema precisa oferecer aos usuários. Cada objetivo ajuda a descobrir responsabilidades que depois serão distribuídas entre as classes.

Relação com a arquitetura de software

Os mesmos princípios valem em uma escala maior. Na arquitetura de software, em vez de classes, falamos de módulos, camadas e serviços. Um módulo coeso reúne funcionalidades de um mesmo assunto; módulos pouco acoplados se comunicam por contratos claros.

Quando os componentes estão bem separados, o sistema fica mais fácil de:

  • manter: uma correção fica restrita à parte responsável;
  • testar: cada componente pode ser verificado isoladamente;
  • evoluir: novas funções entram sem reescrever o que já existe;
  • substituir: uma parte pode ser trocada sem derrubar as outras;
  • compreender: quem chega ao projeto encontra as coisas pelo nome.

Por isso, coesão e acoplamento funcionam como o vocabulário básico da arquitetura.

Exemplo em Python

O código abaixo é uma representação didática, executável, de um cadastro com responsabilidades separadas. Os dados ficam em memória, e as notificações são apenas impressas na tela.

# 1. Repositório: só cuida de guardar e buscar usuários
class UsuarioRepository:
    def __init__(self):
        self._usuarios = {}

    def salvar(self, usuario):
        self._usuarios[usuario["email"]] = usuario

    def buscar_por_email(self, email):
        return self._usuarios.get(email)


# 2. Notificação: só sabe enviar mensagens
class NotificacaoService:
    def enviar(self, destino, mensagem):
        print(f"Para {destino}: {mensagem}")


# 3. Serviço: aplica as regras do cadastro
class UsuarioService:
    def __init__(self, repositorio, notificador):
        # 4. Dependências recebidas de fora
        self.repositorio = repositorio
        self.notificador = notificador

    def cadastrar(self, nome, email):
        # 5. Regras de negócio
        if not nome.strip() or "@" not in email:
            return "Dados inválidos."
        if self.repositorio.buscar_por_email(email):
            return "E-mail já cadastrado."
        # 6. Delega persistência e notificação
        self.repositorio.salvar({"nome": nome, "email": email})
        self.notificador.enviar(email, "Cadastro realizado.")
        return "Cadastro concluído."


# 7. Montagem: escolhemos as implementações
servico = UsuarioService(UsuarioRepository(), NotificacaoService())
print(servico.cadastrar("Maria Silva", "maria@exemplo.com"))
print(servico.cadastrar("Maria Silva", "maria@exemplo.com"))


# 8. Teste: trocamos a notificação por uma versão falsa
class NotificacaoFalsa:
    def __init__(self):
        self.enviadas = []

    def enviar(self, destino, mensagem):
        self.enviadas.append(destino)


falsa = NotificacaoFalsa()
teste = UsuarioService(UsuarioRepository(), falsa)
assert teste.cadastrar("João", "joao@exemplo.com") == "Cadastro concluído."
assert falsa.enviadas == ["joao@exemplo.com"]
print("Teste concluído.")

Passo a passo:

  1. UsuarioRepository tem uma responsabilidade de informação: guardar e buscar usuários. Ele não sabe nada sobre validação ou mensagens.
  2. NotificacaoService tem uma única responsabilidade de comportamento: enviar mensagens.
  3. UsuarioService concentra apenas as regras do cadastro. É a parte que “pensa” sobre o negócio.
  4. O serviço recebe o repositório e o notificador no construtor em vez de criá-los. Isso é o que mantém o acoplamento baixo.
  5. As regras ficam juntas: nome preenchido, e-mail com “@” e ausência de duplicidade.
  6. Depois de validar, o serviço delega: pede ao repositório para salvar e ao notificador para avisar.
  7. A montagem acontece em um ponto só. Na primeira chamada, o cadastro é concluído; na segunda, o mesmo e-mail é recusado.
  8. No teste, entra uma NotificacaoFalsa, que apenas registra os envios. O serviço funciona sem nenhuma alteração, o que mostra na prática o ganho do baixo acoplamento. Veja mais sobre isso em o que são testes de software.

Erros comuns

  • Criar uma classe “faz tudo”: nomes como Sistema ou Gerenciador costumam esconder várias responsabilidades. Divida por assunto.
  • Separar demais sem necessidade: uma classe para cada linha de código torna o sistema difícil de navegar. Separe quando os motivos de mudança forem diferentes.
  • Criar dependências desnecessárias: importar ou instanciar algo “porque pode ser útil” aumenta o acoplamento sem ganho real.
  • Confundir quantidade de classes com qualidade: o que importa é a clareza das responsabilidades, não o número de arquivos.
  • Tentar eliminar todas as dependências: partes precisam colaborar. O objetivo é que as dependências sejam poucas, explícitas e estáveis.
  • Misturar regra de negócio com apresentação ou persistência: validar dados dentro da tela ou escrever consultas ao banco dentro da regra de cadastro prende assuntos diferentes uns aos outros.

Checklist prático

Use estas perguntas ao revisar uma classe, um módulo ou um diagrama:

  • Esta classe tem uma responsabilidade clara?
  • Suas responsabilidades estão relacionadas entre si?
  • Consigo descrevê-la em uma frase, sem vários “e”?
  • Ela depende diretamente de muitos outros componentes?
  • Uma alteração simples exige mudanças em vários lugares?
  • É fácil testar essa parte isoladamente?
  • O nome da classe representa claramente seu papel?

Se várias respostas indicarem problema, não é preciso reescrever tudo de uma vez. Comece extraindo a responsabilidade que mais muda ou que mais atrapalha os testes.

Conclusão

Responsabilidades claras são o ponto de partida de um bom design: cada parte do sistema precisa saber o que deve saber e fazer o que deve fazer. A alta coesão mantém juntas as responsabilidades relacionadas, o que deixa cada classe mais fácil de entender e de testar. O baixo acoplamento reduz as dependências desnecessárias, o que evita o efeito cascata a cada mudança.

Esses conceitos não exigem ferramentas especiais, apenas atenção às perguntas certas durante a modelagem e a programação. Eles também preparam o caminho para decisões maiores de arquitetura e para um sistema que continua fácil de manter enquanto cresce.

Leia também

Voltar à Engenharia de Software