Módulo 11 — Introdução à Modelagem UML

Onde estamos. Nos módulos anteriores, você aprendeu a descobrir e registrar o que um sistema deve fazer. Agora precisamos dar o passo seguinte: representar como esse sistema será organizado por dentro, antes de escrever qualquer linha definitiva. É aqui que entra a modelagem visual. Quero que você chegue à aula entendendo que um bom desenho não é enfeite de relatório, e sim uma ferramenta de pensamento que revela problemas de projeto enquanto ainda são baratos de resolver.

Deixe-me abrir com uma provocação honesta. Você provavelmente já ouviu que “diagrama é perda de tempo, o que vale é o código”. Entendo a origem dessa desconfiança, e ela tem um fundo de verdade: existe muita gente que desenha diagramas lindos e inúteis, que envelhecem numa gaveta enquanto o sistema segue outro rumo. Mas confundir o mau uso de uma ferramenta com a inutilidade dela é um erro caro. Quando você desenha as classes de um sistema antes de codificá-las, está fazendo uma coisa que o código puro dificulta: enxergando a estrutura inteira de uma vez, num nível de abstração em que os erros de organização ficam visíveis. É sobre essa forma de pensar que vamos conversar, e a linguagem que usaremos para isso chama-se UML.

O que é a UML e para que ela serve

A sigla UML significa Unified Modeling Language, ou Linguagem de Modelagem Unificada. Repare em cada palavra, porque elas carregam a história. Ela é unificada porque nasceu da convergência de três abordagens de modelagem orientada a objetos que competiam entre si nos anos 1990, reunidas por seus autores num esforço para acabar com a torre de Babel de notações que existia então. Ela é uma linguagem porque tem vocabulário e regras de sintaxe, ainda que gráficos em vez de textuais. E ela é de modelagem porque seu propósito não é executar nada, e sim descrever, comunicar e raciocinar sobre sistemas.

Quero desfazer um mal-entendido comum antes de seguirmos. A UML não é um método de desenvolvimento, não diz a você em que ordem fazer as coisas, nem promete transformar automaticamente desenhos em programas. Ela é apenas uma linguagem, um conjunto de símbolos compartilhados. Craig Larman, autor que nos acompanhará bastante ao tratar de análise e projeto orientados a objetos, insiste que o valor da UML está menos nos desenhos em si e mais no raciocínio que eles forçam: modelar é uma desculpa produtiva para pensar antes de agir. Bezerra, na mesma linha, trata a UML como o idioma comum que permite a uma equipe discutir projeto sem que cada pessoa entenda uma coisa diferente das mesmas palavras.

ImportanteA ideia central que você deve carregar

Um modelo é uma simplificação intencional da realidade. Ao desenhar um diagrama, você decide o que mostrar e, principalmente, o que ocultar. O poder da modelagem está exatamente nessa omissão deliberada: ela permite raciocinar sobre uma dimensão do sistema — sua estrutura, ou seu comportamento — sem se afogar em todos os detalhes ao mesmo tempo.

Duas famílias: diagramas estruturais e comportamentais

A UML oferece uma boa quantidade de tipos de diagrama, e tentar memorizar todos seria um desperdício de energia. O que importa é entender a divisão que organiza tudo: existem diagramas estruturais e diagramas comportamentais. Os estruturais fotografam o sistema parado, mostram quais peças existem e como elas se conectam, independentemente do tempo. Os comportamentais filmam o sistema em movimento, mostram o que acontece, em que ordem e sob quais condições. É a velha distinção entre a planta de um edifício e o vídeo das pessoas circulando por ele.

A tabela a seguir situa os diagramas que estudaremos neste módulo dentro dessa divisão. Não se preocupe em decorar a lista; preocupe-se em reconhecer a lógica que separa as duas colunas.

Família Pergunta que responde Diagramas que veremos
Estrutural Do que o sistema é feito e como as partes se ligam? Diagrama de classes
Comportamental O que o sistema faz ao longo do tempo? Diagrama de sequência, de atividades e de estados

Concentraremos a maior parte da nossa atenção no diagrama de classes, porque ele é a espinha dorsal da modelagem orientada a objetos e o que mais diretamente se traduz em código. Depois, veremos três diagramas comportamentais que iluminam dimensões que o de classes, por ser estático, não consegue mostrar.

O diagrama de classes em profundidade

O diagrama de classes é o mapa da estrutura de um sistema orientado a objetos. Cada classe aparece como um retângulo dividido em três compartimentos: o nome no topo, os atributos no meio e os métodos embaixo. Os atributos são os dados que cada objeto daquela classe guarda; os métodos são as operações que ele sabe realizar. Essa divisão espelha exatamente a noção de que um objeto é estado mais comportamento reunidos numa única cápsula.

Um detalhe que iniciantes ignoram, mas que carrega intenção de projeto, é a visibilidade. Diante de cada atributo ou método, colocamos um sinal que indica quem pode acessá-lo. O sinal + marca o que é público, visível a todos; o sinal - marca o que é privado, acessível apenas de dentro da própria classe; e o sinal # marca o que é protegido, visível à classe e às suas descendentes. Essa notação é a expressão gráfica do encapsulamento: ao desenhar um atributo como privado, você está afirmando, no nível do projeto, que ninguém de fora deveria mexer nele diretamente.

O que torna o diagrama de classes poderoso, porém, não são as classes isoladas, e sim as relações entre elas. É aqui que mora a parte que exige mais cuidado, porque a UML distingue tipos de relação que se parecem à primeira vista, mas significam coisas diferentes. Vamos percorrê-los com calma.

A associação é a relação mais genérica: significa apenas que objetos de uma classe se conectam a objetos de outra e precisam se conhecer para colaborar. Um Pedido está associado a um Cliente porque um pedido pertence a alguém. A agregação é uma associação especial que expressa uma relação de todo e parte, em que a parte tem vida própria e pode existir sem o todo: um Departamento agrega Professor, mas o professor continua existindo se o departamento for extinto. A composição é uma forma mais forte de todo e parte, em que a parte não faz sentido sem o todo e morre junto com ele: uma Fatura é composta de ItemDeFatura, e apagar a fatura apaga os itens, que não têm existência autônoma. Por fim, a herança, ou generalização, expressa que uma classe é um tipo especializado de outra: Gerente herda de Funcionario porque todo gerente é um funcionário, com algo a mais.

NotaUm teste mental para não confundir as relações

Pergunte “a parte pode viver sem o todo?”. Se sim, e há relação de todo-parte, é agregação. Se não, é composição. Pergunte “esta classe é um tipo de aquela?”. Se a resposta natural for “é um”, trata-se de herança, nunca de associação. E, na dúvida entre agregação e associação simples, prefira a associação: ela diz menos e erra menos.

Cada relação carrega ainda uma multiplicidade, que informa quantos objetos de um lado se ligam a quantos do outro. Escrevemos 1 para exatamente um, 0..1 para no máximo um, 1..* para um ou muitos, e * para qualquer quantidade, inclusive nenhuma. Quando você escreve que um Cliente se relaciona com 0..* Pedido, está afirmando uma regra de negócio inteira num único símbolo: um cliente pode não ter pedido algum ou ter vários. A tabela reúne a notação para consulta.

Relação Significado Notação gráfica
Associação Objetos se conhecem e colaboram Linha sólida
Agregação Todo-parte, parte independente Linha com losango vazado no todo
Composição Todo-parte, parte dependente Linha com losango preenchido no todo
Herança “É um tipo de” Linha com triângulo vazado apontando ao pai

Vejamos tudo isso num diagrama concreto. Modelarei um pequeno domínio de biblioteca, com uma classe base Pessoa da qual descende Membro, um Emprestimo que associa membro e livro, e uma composição entre Emprestimo e seus itens de multa.

classDiagram
    class Pessoa {
        #String nome
        #String documento
        +String descricao()
    }
    class Membro {
        -String matricula
        -bool ativo
        +bool podeEmprestar()
    }
    class Livro {
        -String titulo
        -String isbn
        -bool disponivel
    }
    class Emprestimo {
        -DateTime dataRetirada
        -DateTime? dataDevolucao
        +bool estaAtrasado()
        +double calcularMulta()
    }
    class Multa {
        -double valor
        -bool paga
    }
    Pessoa <|-- Membro
    Membro "1" --> "0..*" Emprestimo : realiza
    Emprestimo "1" --> "1" Livro : refere
    Emprestimo "1" *-- "0..1" Multa : gera
    style Pessoa fill:#cfe2ff,stroke:#084298
    style Membro fill:#d1e7dd,stroke:#0f5132
    style Emprestimo fill:#fff3cd,stroke:#664d03
    style Multa fill:#f8d7da,stroke:#842029

Observe o que cada símbolo comunica. O triângulo entre Pessoa e Membro diz que membro é um tipo de pessoa. A seta de Membro para Emprestimo com multiplicidade 1 e 0..* diz que um membro realiza de nenhum a muitos empréstimos. O losango preenchido entre Emprestimo e Multa declara composição: a multa não existe sem o empréstimo que a originou. Um único desenho carrega, assim, um punhado de regras de negócio que, em prosa, exigiriam parágrafos.

Do diagrama ao código Dart

A força do diagrama de classes está em como ele se converte quase mecanicamente em código. Cada classe vira uma class; cada atributo, um campo com o modificador de acesso correspondente; cada método, uma função; e cada relação, uma referência de um objeto a outro, com a multiplicidade determinando se o campo é um objeto único ou uma coleção. Vou implementar fielmente o diagrama acima para você ver a correspondência linha a linha. Em Dart, o encapsulamento privado se marca com o prefixo _ no identificador, que faz as vezes do - da UML.

abstract class Pessoa {
  final String nome;
  final String documento;

  Pessoa(this.nome, this.documento);

  String descricao() => '$nome ($documento)';
}

class Membro extends Pessoa {
  final String _matricula;
  bool _ativo;
  final List<Emprestimo> emprestimos = [];

  Membro(super.nome, super.documento, this._matricula, {bool ativo = true})
      : _ativo = ativo;

  bool podeEmprestar() => _ativo && emprestimos.where((e) => e.estaAtrasado()).isEmpty;

  void registrar(Emprestimo e) => emprestimos.add(e);
}

class Livro {
  final String titulo;
  final String isbn;
  bool disponivel;

  Livro(this.titulo, this.isbn, {this.disponivel = true});
}

class Emprestimo {
  final Livro livro;
  final DateTime dataRetirada;
  DateTime? dataDevolucao;
  Multa? multa; // composição: nasce e morre com o empréstimo

  Emprestimo(this.livro, this.dataRetirada);

  bool estaAtrasado() {
    final referencia = dataDevolucao ?? DateTime.now();
    return referencia.difference(dataRetirada).inDays > 14;
  }

  double calcularMulta() {
    final referencia = dataDevolucao ?? DateTime.now();
    final atraso = referencia.difference(dataRetirada).inDays - 14;
    if (atraso <= 0) return 0.0;
    multa = Multa(atraso * 1.5);
    return multa!.valor;
  }
}

class Multa {
  final double valor;
  bool paga;

  Multa(this.valor, {this.paga = false});
}

Compare este código com o diagrama e note que não há surpresa: a herança virou extends, a composição virou um campo Multa? criado dentro do próprio Emprestimo, e a multiplicidade 0..* de empréstimos virou uma List. Bezerra chama a atenção para essa proximidade justamente porque ela explica por que investimos em modelar antes: um erro na estrutura das classes, detectado no desenho, custa a borracha de um traço; o mesmo erro descoberto depois de codificado custa refatoração.

O diagrama de sequência: interação no tempo

O diagrama de classes é mudo quanto ao tempo. Ele diz quais objetos existem, mas não conta quem chama quem, nem em que ordem. Essa lacuna é preenchida pelo diagrama de sequência, um diagrama comportamental que mostra a colaboração entre objetos como uma conversa ordenada de cima para baixo. Cada objeto ganha uma linha de vida vertical, e as mensagens trocadas entre eles aparecem como setas horizontais, na sequência exata em que ocorrem. É o diagrama que você desenha quando quer entender ou explicar como um caso de uso se realiza por dentro, distribuído entre os objetos que colaboram.

Larman valoriza especialmente esse diagrama porque ele torna visível a atribuição de responsabilidades: ao decidir qual objeto envia qual mensagem a qual outro, você está decidindo quem faz o quê no sistema, e essa é a essência do bom projeto orientado a objetos. Vejamos a sequência de devolver um livro, seguindo o mesmo domínio de biblioteca.

sequenceDiagram
    actor Atendente
    participant M as Membro
    participant E as Emprestimo
    participant L as Livro
    Atendente->>M: devolver(emprestimo)
    M->>E: dataDevolucao = hoje
    M->>E: calcularMulta()
    E-->>M: valor da multa
    M->>L: disponivel = true
    M-->>Atendente: comprovante

Repare em dois detalhes de leitura. As setas cheias representam chamadas de mensagem, e as setas tracejadas representam retornos. A ordem vertical é a ordem temporal, sem ambiguidade. Um diagrama assim revela cedo problemas que o de classes esconde, como um objeto que precisaria conhecer outro que, na estrutura, ele não tem como alcançar.

O diagrama de atividades: o fluxo de trabalho

Enquanto o diagrama de sequência foca a troca de mensagens entre objetos específicos, o diagrama de atividades sobe um degrau de abstração e descreve um fluxo de trabalho ou processo, com suas decisões, ramificações e ações em paralelo. Ele se parece com um fluxograma, mas com vocabulário próprio: um nó inicial, ações representadas por retângulos arredondados, losangos de decisão que abrem caminhos condicionais, e um nó final. É o diagrama ideal para descrever a lógica de um processo de negócio ou de um algoritmo antes de codificá-lo, porque expressa “primeiro isto, depois aquilo, se tal condição então por aqui”.

flowchart TD
    I((início)) --> A[Solicitar empréstimo]
    A --> D{Membro pode emprestar?}
    D -- não --> R[Recusar solicitação]
    D -- sim --> C{Livro disponível?}
    C -- não --> R
    C -- sim --> E[Registrar empréstimo]
    E --> F[Marcar livro indisponível]
    F --> G((fim))
    R --> G
    style D fill:#fff3cd,stroke:#664d03
    style C fill:#fff3cd,stroke:#664d03
    style E fill:#d1e7dd,stroke:#0f5132
    style R fill:#f8d7da,stroke:#842029

O ganho aqui é enxergar o processo inteiro, com todas as suas bifurcações, numa só imagem. Quando você desenha os losangos de decisão, é obrigado a se perguntar o que acontece em cada ramo, e é comum descobrir, nesse momento, um caminho que ninguém havia pensado — exatamente o tipo de omissão que sai caro se só aparecer em produção.

O diagrama de estados: o ciclo de vida de um objeto

Resta um ângulo que nenhum diagrama anterior captura: como um único objeto muda de condição ao longo de sua vida. Um Emprestimo não é uma coisa fixa; ele nasce ativo, pode ficar atrasado, e termina devolvido. Cada uma dessas condições é um estado, e o que leva de um estado a outro é um evento ou transição. O diagrama de estados desenha esse ciclo de vida como uma máquina: bolinhas arredondadas para os estados, setas rotuladas para as transições, um ponto de partida e, quando existe, um ponto final. Ele é o diagrama certo sempre que um objeto se comporta de forma diferente dependendo da situação em que se encontra.

stateDiagram-v2
    [*] --> Ativo : criado
    Ativo --> Atrasado : prazo excedido
    Ativo --> Devolvido : devolver()
    Atrasado --> Devolvido : devolver() com multa
    Devolvido --> [*]

Note como esse diagrama codifica regras que seriam trabalhosas de expressar de outra forma: não existe seta de Devolvido de volta para Ativo, o que declara que um empréstimo devolvido não pode ser reativado. Modelar estados é, muitas vezes, a maneira mais limpa de descobrir e comunicar quais transições o sistema deve permitir e, sobretudo, quais deve proibir.

Perceba a divisão de trabalho entre os quatro diagramas que vimos. O de classes mostra a estrutura estática; o de sequência, a colaboração entre objetos no tempo; o de atividades, o fluxo de um processo; e o de estados, o ciclo de vida de um objeto isolado. Escolher o diagrama certo é escolher a pergunta que você quer responder. Desenhar todos por obrigação é o erro que dá má fama à modelagem.

Modelar é decidir o que não desenhar

Este módulo abre a frente de projeto e modelagem que vínhamos anunciando. Aqui você conheceu a UML como linguagem, a divisão entre diagramas estruturais e comportamentais, e os quatro tipos que mais usará na prática. Adiante, ao estudarmos padrões de projeto e qualidade de código, você verá esses diagramas reaparecerem como ferramenta de comunicação. A modelagem não substitui o código; ela o antecede e o esclarece.

Síntese

Quero que você retenha três ideias desta conversa. A primeira é que a UML é uma linguagem, não um método nem um gerador de código: seu valor está no raciocínio disciplinado que ela força, e um modelo bem-feito vale pela clareza que dá à equipe, como insistem Bezerra e Larman. A segunda é a divisão fundamental entre diagramas estruturais, que fotografam o sistema parado, e comportamentais, que o filmam em ação; dominar essa distinção já organiza mentalmente todo o resto. A terceira é que o diagrama de classes se traduz quase mecanicamente em código, com herança, associação, agregação e composição virando construções concretas na linguagem, e por isso um erro corrigido no desenho é incomparavelmente mais barato do que o mesmo erro corrigido depois de implementado.

Para consolidar antes da aula: escolha um pequeno domínio que você conheça bem, como uma agenda de contatos ou um carrinho de compras, e desenhe seu diagrama de classes com pelo menos uma associação, uma composição e uma herança, marcando visibilidades e multiplicidades. Depois, traduza uma das classes para Dart. Se o código sair sem esforço a partir do desenho, você entendeu o essencial deste módulo.