Modelando um sistema antes de programar6.1
Hoje você vai ver como UML ajuda a transformar uma necessidade de negócio em uma estrutura de software que pode, de fato, virar código e banco de dados sem improviso.
Quando alguém começa a implementar um sistema direto no editor, sem modelar, quase sempre aparecem os mesmos problemas. Uma classe começa a assumir responsabilidades demais, os nomes ficam imprecisos, duas pessoas da equipe entendem a mesma funcionalidade de formas diferentes e o banco de dados nasce como uma cópia mal pensada do código ou, pior, como uma lista de tabelas sem relação clara com o domínio. UML existe justamente para reduzir esse tipo de ruído.
Ao longo desta aula, você vai acompanhar um único domínio de exemplo do começo ao fim. Em vez de ver vários desenhos soltos, vamos evoluir o sistema do Pet Shop "Amigo Fiel" em etapas coerentes. Primeiro veremos o que o sistema precisa oferecer para seus usuários. Depois veremos quais classes do domínio surgem naturalmente. Em seguida, acompanharemos a colaboração entre os objetos em um cenário concreto. Por fim, mapearemos esse mesmo modelo para um banco de dados relacional.
O ponto central desta aula é simples e muito importante. Um diagrama bom não é um enfeite. Ele serve para responder uma pergunta específica sobre o sistema.
Se você precisasse implementar um sistema de agendamento para um pet shop ainda hoje, por onde começaria? Pelas telas, pelas tabelas do banco, pelas classes em C++ ou pelas funcionalidades que o negócio realmente precisa?
Se você respondeu "depende do que quero enxergar", então já está pensando do jeito certo. É exatamente isso que a modelagem faz. Cada diagrama mostra uma perspectiva diferente do mesmo sistema.
O domínio único da aula6.2
Usaremos o sistema do Pet Shop "Amigo Fiel". O estabelecimento realiza banho, tosa e consulta veterinária. Hoje, os agendamentos são controlados em papel. O recepcionista anota o nome do cliente, o pet, o serviço e o horário em uma agenda manual. Isso gera conflitos de horário, perda de histórico e dificuldade para saber quem atenderá cada animal.
Nosso sistema precisa permitir pelo menos estas ações:
- cadastrar clientes
- cadastrar pets vinculados a seus donos
- consultar disponibilidade
- agendar serviços
- registrar o atendimento realizado
- finalizar a cobrança
Perceba que essas ações ainda estão em um nível de negócio. Neste momento, não estamos falando em class, struct, vector, chave estrangeira ou consulta SQL. Estamos apenas delimitando o problema. Esse é o primeiro cuidado importante em UML. Antes de discutir implementação, você precisa entender o que o sistema deve fazer.
Da necessidade do negócio para o Diagrama de Casos de Uso6.3
O primeiro passo natural é representar a visão externa do sistema. Aqui não queremos saber como os objetos conversam internamente, nem como o banco será desenhado. Queremos apenas responder uma pergunta fundamental: quais serviços o sistema oferece para os atores externos?
O que o Diagrama de Casos de Uso mostra6.3.1
O Diagrama de Casos de Uso mostra as funcionalidades percebidas pelos usuários ou por sistemas externos. Ele é útil quando você ainda está organizando requisitos funcionais, validando escopo com o cliente e separando claramente o que está dentro e o que está fora do sistema.
Você deve usar esse diagrama quando quiser:
- identificar funcionalidades centrais do sistema
- conversar com pessoas de negócio sem mergulhar em detalhes técnicos
- delimitar o escopo do software
- preparar a passagem de requisitos para análise e projeto
O erro comum aqui é tratar caso de uso como se fosse passo de algoritmo. "Validar CPF" ou "salvar no banco" não são bons casos de uso isolados neste contexto. Esses são passos internos de uma funcionalidade maior. Caso de uso descreve uma meta relevante para o ator, como "Agendar serviço".
include e extend6.3.2
Uma das maiores dúvidas costuma aparecer justamente aqui. Afinal, quando faz sentido ligar um caso de uso a outro com include ou extend? E mais importante ainda, o que essas palavras realmente querem dizer?
Você deve pensar nessas duas relações como formas de explicar dependência entre funcionalidades, mas não do mesmo jeito.
O que significa include6.3.2.1
include indica que um caso de uso sempre incorpora outro caso de uso como parte obrigatória do seu comportamento. Em outras palavras, o caso principal não está completo sem executar aquele trecho incluído.
Use include quando quiser representar uma funcionalidade que:
- é obrigatória dentro de outra
- pode ser reaproveitada por mais de um caso de uso
- faz parte do fluxo normal, não de uma exceção
No nosso Pet Shop, Agendar Serviço inclui Consultar Disponibilidade. O motivo é simples. Para agendar corretamente, o sistema necessariamente precisa verificar se existe horário livre. Não é um caminho opcional. É parte obrigatória do agendamento.
Uma forma prática de testar isso é fazer a seguinte pergunta mental: o caso de uso principal consegue terminar corretamente sem esse outro? Se a resposta for não, include é um forte candidato.
O que significa extend6.3.2.2
extend indica que um caso de uso adiciona um comportamento opcional ou condicional a outro caso de uso. Aqui, o caso base já faz sentido sozinho. A extensão só acontece em certas situações.
Use extend quando quiser representar uma funcionalidade que:
- não acontece sempre
- depende de condição, exceção ou regra específica
- complementa um caso de uso base que já existe por si só
No Pet Shop, Aplicar Desconto pode estender Finalizar Cobrança. A cobrança existe mesmo sem desconto. O desconto só entra em alguns casos, por exemplo quando o cliente é recorrente, quando há promoção ativa ou quando o administrador autoriza um abatimento.
Aqui a pergunta mental muda um pouco. Você deve pensar assim: o caso de uso base já funciona sozinho, e esse outro só entra quando uma condição específica acontece? Se sim, extend costuma ser a escolha certa.
Diferença prática entre os dois6.3.2.3
includerepresenta uma parte obrigatória e reutilizável do fluxoextendrepresenta uma ampliação opcional ou condicional do fluxo
Em linguagem simples para lembrar na hora da prova ou do exercício:
include= "isso sempre acontece junto"extend= "isso só acontece em alguns casos"
Observação sobre ferramentas6.3.2.4
Nem toda ferramenta de diagrama trata include e extend do mesmo jeito. Em ferramentas UML mais específicas, essas relações costumam ter suporte mais explícito. Já em ferramentas mais genéricas de renderização, como algumas variações de Mermaid, pode acontecer de o texto aparecer apenas como um rótulo visual sem diferença real de semântica ou de estilo.
Por isso, o mais importante para você, como estudante, não é decorar uma sintaxe específica da ferramenta. O mais importante é compreender a ideia conceitual.
- quando uma funcionalidade incorpora obrigatoriamente outra, você está no terreno de
include - quando uma funcionalidade estende condicionalmente outra, você está no terreno de
extend
Em outras palavras, algumas ferramentas dão suporte melhor a essa distinção e outras apenas ajudam a desenhá-la visualmente. O que não pode faltar no seu diagrama é a clareza da relação que você quer comunicar.
Exemplo isolado para treinar a leitura6.3.2.5
Antes de voltar ao diagrama principal, veja um exemplo mínimo só para fixar a diferença.
graph LR
R[Recepcionista]
subgraph Sistema[Pet Shop Amigo Fiel]
UC1([Agendar Serviço])
UC2([Consultar Disponibilidade])
UC3([Finalizar Cobrança])
UC4([Aplicar Desconto])
end
R --> UC1
R --> UC3
UC1 -.-> UC2
UC4 -.-> UC3 Nesse exemplo, Agendar Serviço depende obrigatoriamente de Consultar Disponibilidade, então estamos diante de uma relação de incorporação obrigatória, isto é, a ideia de include. Já Aplicar Desconto não acontece em toda cobrança, então temos uma ampliação condicional do caso base Finalizar Cobrança, isto é, a ideia de extend.
Erro comum ao usar essas relações6.3.2.6
Um erro muito comum é sair conectando casos de uso com include e extend para qualquer passo pequeno do sistema. Isso torna o diagrama confuso e burocrático. Nem toda ação interna merece virar um caso de uso separado.
Por exemplo, Salvar no Banco, Validar Telefone ou Buscar Cliente por ID normalmente não devem aparecer como casos de uso. Esses são detalhes internos de implementação ou passos pequenos demais. Casos de uso devem continuar representando objetivos perceptíveis para o ator.
Outro erro comum é usar extend quando a funcionalidade na verdade é obrigatória. Se o sistema sempre consulta a agenda antes de marcar um horário, isso não é extensão opcional. Isso é include.
Primeira versão do modelo6.3.3
Vamos começar com uma visão simples do Pet Shop. Temos três atores principais:
- Recepcionista
- Veterinário/Esteticista
- Administrador
E temos as funcionalidades mais visíveis para eles.
graph LR
R[Recepcionista]
V[Veterinário / Esteticista]
A[Administrador]
subgraph Sistema[Pet Shop Amigo Fiel]
UC1([Cadastrar Cliente])
UC2([Cadastrar Pet])
UC3([Consultar Disponibilidade])
UC4([Agendar Serviço])
UC5([Registrar Atendimento])
UC6([Finalizar Cobrança])
UC7([Emitir Relatório Diário])
end
R --> UC1
R --> UC2
R --> UC3
R --> UC4
V --> UC5
A --> UC7
R --> UC6
UC4 -.->|<include>| UC3 Como ler esse diagrama6.3.4
O Recepcionista conversa com quase todo o fluxo operacional. Ele cadastra o cliente, cadastra o pet e agenda o serviço. O caso de uso Agendar Serviço inclui Consultar Disponibilidade porque, no nosso domínio, não faz sentido concluir um agendamento sem antes verificar horário livre. Já o Veterinário ou Esteticista participa quando o atendimento realmente acontece. O Administrador, por sua vez, está mais ligado à supervisão e aos relatórios.
Esse diagrama não mostra classes, tabelas ou métodos. E isso é uma qualidade, não uma limitação. Ele está no nível certo para responder à pergunta "o que o sistema oferece?".
Como isso começa a aparecer em C++6.3.5
Mesmo sem definir ainda o modelo completo, já conseguimos imaginar alguns pontos de entrada do sistema em código orientado a objetos. Um sistema em C++ pode ter uma camada de aplicação com operações que correspondem a esses casos de uso.
class PetShopSystem {
public:
void cadastrarCliente(const std::string& nome, const std::string& telefone);
void cadastrarPet(int clienteId, const std::string& nomePet, const std::string& especie);
bool consultarDisponibilidade(const std::string& dataHora) const;
int agendarServico(int petId, int servicoId, int funcionarioId, const std::string& dataHora);
void registrarAtendimento(int agendamentoId, const std::string& observacoes);
void finalizarCobranca(int atendimentoId);
};
Repare no vínculo conceitual. Ainda não sabemos quais classes internas serão usadas, mas já enxergamos operações que representam os serviços oferecidos pelo sistema. Casos de uso ajudam a nomear responsabilidades. Eles funcionam como uma ponte entre requisito e interface de software.
Refinando o modelo6.3.6
Até aqui, o caso de uso nos mostrou o comportamento externo. O próximo passo é perguntar algo mais estrutural. Se o sistema precisa cadastrar clientes, pets, serviços e agendamentos, quais entidades do domínio realmente existem? Em outras palavras, quais objetos precisam existir no código para que esses casos de uso possam acontecer?
É exatamente essa pergunta que leva ao Diagrama de Classes.
Do comportamento externo para o Diagrama de Classes6.4
Depois de identificar o que o sistema faz, precisamos entender de que elementos ele é composto. Aqui saímos da visão do usuário e passamos para a visão estrutural do software.
O que o Diagrama de Classes mostra6.4.1
O Diagrama de Classes mostra os principais elementos do domínio, seus atributos, seus métodos e os relacionamentos entre eles. Você deve usá-lo quando quiser organizar a estrutura estática do sistema, discutir responsabilidades de cada classe, avaliar associações, herança, composição e cardinalidade.
Se o Diagrama de Casos de Uso responde "o que o sistema oferece", o Diagrama de Classes responde "quais objetos precisam existir e como eles se relacionam".
Primeira extração de classes a partir dos casos de uso6.4.2
Do diagrama anterior, já conseguimos extrair candidatos naturais a classes do domínio:
Cliente, porque o sistema cadastra clientesPet, porque cada cliente pode ter animais cadastradosServico, porque o pet shop oferece tipos de serviçoAgendamento, porque o serviço é marcado para uma data e horaFuncionario, porque alguém executa o serviço
Começaremos com uma versão inicial, ainda enxuta.
classDiagram
direction LR
class Cliente {
-int id
-string nome
-string telefone
+adicionarPet(pet)
}
class Pet {
-int id
-string nome
-string especie
-int idade
}
class Servico {
-int id
-string descricao
-double precoBase
}
class Funcionario {
-int id
-string nome
-string especialidade
}
class Agendamento {
-int id
-string dataHora
-string status
+confirmar()
+cancelar()
}
Cliente "1" --> "0..*" Pet : possui
Pet "1" --> "0..*" Agendamento : gera
Agendamento "1" --> "1" Servico : solicita
Agendamento "1" --> "1" Funcionario : atribui Como ler as cardinalidades6.4.3
Agora a modelagem ficou mais precisa. Um Cliente pode possuir zero ou muitos Pet. Cada Pet pode gerar zero ou muitos Agendamento ao longo do tempo. Cada Agendamento se refere a exatamente um Servico e a exatamente um Funcionario na versão inicial do nosso sistema.
Aqui aparece uma decisão de modelagem importante. Estamos dizendo que cada agendamento tem apenas um serviço. Essa simplificação é útil para aprender a notação e para manter a primeira versão do modelo clara. Em um sistema real, o pet poderia fazer banho e tosa no mesmo horário. Mais adiante veremos como esse refinamento impacta o banco de dados.
Como esse diagrama vira código C++6.4.4
Agora já é possível escrever classes concretas com atributos e associações coerentes com o modelo.
#include <memory>
#include <string>
#include <vector>
class Pet;
class Cliente {
private:
int id;
std::string nome;
std::string telefone;
std::vector<std::shared_ptr<Pet>> pets;
public:
Cliente(int id, std::string nome, std::string telefone)
: id(id), nome(std::move(nome)), telefone(std::move(telefone)) {}
void adicionarPet(const std::shared_ptr<Pet>& pet) {
pets.push_back(pet);
}
};
class Pet {
private:
int id;
std::string nome;
std::string especie;
int idade;
public:
Pet(int id, std::string nome, std::string especie, int idade)
: id(id), nome(std::move(nome)), especie(std::move(especie)), idade(idade) {}
};
class Servico {
private:
int id;
std::string descricao;
double precoBase;
public:
Servico(int id, std::string descricao, double precoBase)
: id(id), descricao(std::move(descricao)), precoBase(precoBase) {}
};
class Funcionario {
private:
int id;
std::string nome;
std::string especialidade;
public:
Funcionario(int id, std::string nome, std::string especialidade)
: id(id), nome(std::move(nome)), especialidade(std::move(especialidade)) {}
};
class Agendamento {
private:
int id;
std::string dataHora;
std::string status;
std::shared_ptr<Pet> pet;
std::shared_ptr<Servico> servico;
std::shared_ptr<Funcionario> funcionario;
public:
Agendamento(int id,
std::string dataHora,
std::shared_ptr<Pet> pet,
std::shared_ptr<Servico> servico,
std::shared_ptr<Funcionario> funcionario)
: id(id),
dataHora(std::move(dataHora)),
status("AGENDADO"),
pet(std::move(pet)),
servico(std::move(servico)),
funcionario(std::move(funcionario)) {}
void confirmar() { status = "CONFIRMADO"; }
void cancelar() { status = "CANCELADO"; }
};
Observe a tradução direta do modelo para a implementação.
- atributos UML viraram atributos privados em C++
- operações UML viraram métodos públicos
- associações 1 para muitos viraram
std::vector - referências entre objetos viraram
std::shared_ptr
Essa passagem do diagrama para o código é uma das razões pelas quais o Diagrama de Classes é tão valioso. Ele não apenas documenta. Ele também orienta a implementação.
Refinamento do modelo estrutural6.4.5
Nossa primeira versão ainda mistura duas ideias que, no domínio real, são diferentes. Uma coisa é o compromisso futuro de atender um pet em certo horário. Outra coisa é o atendimento efetivamente realizado, com observações, horário de início, horário de fim e valor cobrado.
Por isso, vamos evoluir o modelo. Em vez de guardar tudo em Agendamento, criaremos também a classe Atendimento.
classDiagram
direction LR
class Cliente {
-int id
-string nome
-string telefone
+adicionarPet(pet)
}
class Pet {
-int id
-string nome
-string especie
-int idade
}
class Servico {
-int id
-string descricao
-double precoBase
}
class Funcionario {
-int id
-string nome
-string especialidade
}
class Agendamento {
-int id
-string dataHora
-string status
+confirmar()
+cancelar()
+gerarAtendimento()
}
class Atendimento {
-int id
-string inicio
-string fim
-string observacoes
-double valorCobrado
+encerrar()
}
Cliente "1" --> "0..*" Pet : possui
Pet "1" --> "0..*" Agendamento : agenda
Agendamento "1" --> "1" Servico : solicita
Agendamento "1" --> "1" Funcionario : atribui
Agendamento "1" --> "0..1" Atendimento : gera Este refinamento melhora o modelo porque separa planejamento de execução. Isso é essencial tanto para o código quanto para o banco. No código, evita classes infladas. No banco, evita colunas que só fazem sentido depois do atendimento ter começado.
Quando a estrutura não basta: Diagrama de Sequência6.5
Até aqui, já sabemos quais funcionalidades existem e quais objetos principais participam do domínio. Mas ainda falta responder outra pergunta essencial. Quando o recepcionista agenda um serviço, em que ordem as mensagens acontecem entre os objetos?
O Diagrama de Classes mostra quem existe. O Diagrama de Sequência mostra quem chama quem ao longo do tempo.
O que o Diagrama de Sequência mostra6.5.1
O Diagrama de Sequência representa interações dinâmicas entre atores e objetos em um cenário específico. Você deve usá-lo quando quiser detalhar um fluxo de execução, esclarecer responsabilidade entre classes, evitar métodos excessivamente acoplados e validar se o desenho estrutural realmente sustenta o comportamento esperado.
Ele é especialmente útil depois do Diagrama de Classes, porque ajuda a verificar se as operações imaginadas fazem sentido na prática. Em outras palavras, o diagrama de classes propõe a estrutura, e o de sequência testa essa estrutura em movimento.
Cenário escolhido: agendar um serviço6.5.2
Vamos detalhar o caso de uso Agendar Serviço. Para isso, refinaremos um pouco a arquitetura do código. Em vez de mandar tudo para uma classe monolítica chamada PetShopSystem, vamos trabalhar com serviços de aplicação e repositórios.
Participantes do cenário:
Recepcionista, como ator externoTelaAgendamento, representando a interface do sistemaAgendamentoService, contendo a regra de negócioAgendaRepository, para consulta e persistênciaAgendamento, como objeto criado
sequenceDiagram
actor R as Recepcionista
participant T as TelaAgendamento
participant S as AgendamentoService
participant Repo as AgendaRepository
participant A as Agendamento
R->>T: informar pet, serviço, funcionário e horário
T->>S: solicitarAgendamento(petId, servicoId, funcionarioId, dataHora)
S->>Repo: verificarDisponibilidade(funcionarioId, dataHora)
Repo-->>S: disponível
S->>A: criar(status = AGENDADO)
S->>Repo: salvar(A)
Repo-->>S: idGerado
S-->>T: agendamentoCriado(idGerado)
T-->>R: exibir confirmação O que esse diagrama ensina que o anterior não mostrava6.5.3
O Diagrama de Classes nos dizia que Agendamento existe e se relaciona com Pet, Servico e Funcionario. Mas ele não nos dizia em que ordem a disponibilidade seria consultada, nem quem seria responsável por criar o objeto, nem quem faria a persistência. Essas respostas surgem aqui.
Repare em uma decisão de projeto importante. A tela não fala diretamente com o banco. Ela delega para AgendamentoService. Isso preserva a separação de responsabilidades. A regra de negócio fica centralizada em um serviço de aplicação, e o acesso a dados fica encapsulado em um repositório.
Como esse fluxo aparece em C++6.5.4
Agora já conseguimos escrever um recorte de código que corresponda quase linha por linha ao diagrama de sequência.
class AgendaRepository {
public:
bool verificarDisponibilidade(int funcionarioId, const std::string& dataHora) const;
int salvar(const Agendamento& agendamento);
};
class AgendamentoService {
private:
AgendaRepository& repository;
public:
explicit AgendamentoService(AgendaRepository& repository) : repository(repository) {}
int solicitarAgendamento(int petId,
int servicoId,
int funcionarioId,
const std::string& dataHora) {
if (!repository.verificarDisponibilidade(funcionarioId, dataHora)) {
throw std::runtime_error("Horário indisponível para o funcionário informado");
}
Agendamento agendamento(/* id */ 0,
dataHora,
buscarPet(petId),
buscarServico(servicoId),
buscarFuncionario(funcionarioId));
return repository.salvar(agendamento);
}
private:
std::shared_ptr<Pet> buscarPet(int petId);
std::shared_ptr<Servico> buscarServico(int servicoId);
std::shared_ptr<Funcionario> buscarFuncionario(int funcionarioId);
};
O valor didático aqui é enorme. O diagrama não é apenas um desenho bonito do que já sabíamos. Ele ajuda a distribuir responsabilidades de forma melhor. Em vez de enfiar tudo dentro de main() ou dentro de uma única classe gigantesca, o diagrama orienta a decomposição do comportamento.
Refinamento do cenário6.5.5
Podemos ir além. Se o funcionário estiver ocupado, o fluxo não segue para criação do agendamento. Esse é um bom momento para lembrar que diagramas de sequência costumam modelar um cenário principal. Já variações, desvios e decisões mais processuais podem ser representados de forma mais clara em um Diagrama de Atividades.
Entendendo o processo com Diagrama de Atividades6.6
Agora saímos da pergunta "quem troca mensagens com quem" e voltamos a uma pergunta mais operacional. Qual é o fluxo do processo de atendimento desde a chegada do cliente até o início do serviço?
O que o Diagrama de Atividades mostra6.6.1
O Diagrama de Atividades modela fluxo de controle e decisões em um processo. Você deve usá-lo quando quiser explicar etapas, ramificações, validações e caminhos alternativos de execução. Ele é excelente para modelar rotinas de negócio, processos organizacionais e fluxos de interface.
Enquanto o Diagrama de Sequência é centrado em objetos e mensagens, o Diagrama de Atividades é centrado em ações e decisões. Os dois podem representar o mesmo pedaço do sistema, mas com objetivos diferentes.
Processo do Pet Shop já com refinamento6.6.2
Agora que já conhecemos melhor o domínio, podemos modelar o fluxo de recepção de forma mais precisa do que no início do curso.
flowchart TD
A([Início]) --> B[Recepcionista identifica cliente e pet]
B --> C{Cliente já está cadastrado?}
C -- Não --> D[Cadastrar cliente]
D --> E[Cadastrar pet]
C -- Sim --> F[Consultar dados do pet]
E --> G{Há horário disponível?}
F --> G
G -- Não --> H[Oferecer novo horário]
H --> I{Cliente aceita reagendar?}
I -- Sim --> J[Registrar novo agendamento]
I -- Não --> K[Encerrar solicitação]
G -- Sim --> L[Confirmar serviço e funcionário]
L --> M[Registrar agendamento]
M --> N[No dia do atendimento, confirmar chegada do pet]
N --> O{Pet compareceu?}
O -- Não --> P[Marcar agendamento como cancelado]
O -- Sim --> Q[Iniciar atendimento]
J --> R([Fim])
K --> R
P --> R
Q --> R Como relacionar esse fluxo com o código6.6.3
O Diagrama de Atividades costuma ser um excelente ponto de partida para identificar regras de decisão que depois precisam virar if, validações e mudanças de estado no código.
Por exemplo:
- a decisão "Cliente já está cadastrado?" pode virar uma consulta em repositório
- a decisão "Há horário disponível?" vira uma regra em
AgendamentoService - a decisão "Pet compareceu?" impacta o atributo
statusdeAgendamento
Em C++, isso pode aparecer assim:
void confirmarChegadaDoPet(Agendamento& agendamento, bool compareceu) {
if (!compareceu) {
agendamento.cancelar();
return;
}
agendamento.confirmar();
}
Claro que, em um sistema real, talvez você preferisse estados mais específicos como EM_ATENDIMENTO, CONCLUIDO e CANCELADO. O importante aqui é notar como o fluxo do processo ajuda a explicitar transições que depois precisam ser implementadas de forma controlada.
Refinando o que aprendemos até aqui6.6.4
Neste ponto, nosso modelo já amadureceu bastante.
- o caso de uso mostrou as metas do usuário
- o diagrama de classes mostrou as entidades do domínio
- o diagrama de sequência mostrou a colaboração entre objetos
- o diagrama de atividades mostrou o fluxo operacional e os caminhos alternativos
Falta agora uma pergunta essencial para qualquer sistema real. Como persistir tudo isso em banco de dados?
Do modelo de classes para o banco de dados6.7
Muita gente aprende UML e banco de dados como se fossem assuntos separados. Não deveriam ser. Em projetos orientados a objetos com persistência relacional, a modelagem fica muito mais clara quando você percebe que várias decisões do Diagrama de Classes influenciam diretamente o esquema relacional.
Como mapear classes para tabelas6.7.1
Em nosso exemplo, o mapeamento mais direto é este:
Clientevira a tabelaclientesPetvira a tabelapetsFuncionariovira a tabelafuncionariosServicovira a tabelaservicosAgendamentovira a tabelaagendamentosAtendimentovira a tabelaatendimentos
Esse tipo de mapeamento é natural quando as classes representam entidades persistentes do domínio. Atributos simples viram colunas. Identificadores viram chaves primárias. Associações viram chaves estrangeiras ou, dependendo da cardinalidade, tabelas de associação.
Relações e cardinalidade no modelo relacional6.7.2
Veja como algumas cardinalidades do nosso Diagrama de Classes se traduzem:
Cliente 1 -> 0..* Petsignifica que a tabelapetsdeve ter uma chave estrangeira apontando paraclientesPet 1 -> 0..* Agendamentosignifica que a tabelaagendamentosdeve referenciarpetsAgendamento 1 -> 1 Servicosignifica queagendamentostambém referenciaservicosAgendamento 1 -> 1 Funcionariosignifica queagendamentosreferenciafuncionariosAgendamento 1 -> 0..1 Atendimentosignifica queatendimentosreferenciaagendamentose pode haver no máximo um atendimento por agendamento
Em termos práticos, isso quer dizer que a multiplicidade do mundo orientado a objetos precisa ser preservada no banco por meio de restrições estruturais adequadas.
Diagrama ER do Pet Shop6.7.3
Agora vamos montar o diagrama entidade-relacionamento correspondente ao modelo refinado.
erDiagram
CLIENTES ||--o{ PETS : possui
PETS ||--o{ AGENDAMENTOS : gera
SERVICOS ||--o{ AGENDAMENTOS : classifica
FUNCIONARIOS ||--o{ AGENDAMENTOS : executa
AGENDAMENTOS ||--o| ATENDIMENTOS : origina
CLIENTES {
int id PK
string nome
string telefone
}
PETS {
int id PK
int cliente_id FK
string nome
string especie
int idade
}
SERVICOS {
int id PK
string descricao
decimal preco_base
}
FUNCIONARIOS {
int id PK
string nome
string especialidade
}
AGENDAMENTOS {
int id PK
int pet_id FK
int servico_id FK
int funcionario_id FK
string data_hora
string status
}
ATENDIMENTOS {
int id PK
int agendamento_id FK
string inicio
string fim
string observacoes
decimal valor_cobrado
} O que esse diagrama preserva do modelo UML6.7.4
O diagrama ER não nasceu do nada. Ele é a continuação lógica do que já havíamos modelado em UML.
Cliente e Pet continuam relacionados da mesma forma, mas agora esse vínculo aparece como cliente_id em pets. Agendamento continua associado a Pet, Servico e Funcionario, e isso aparece como três chaves estrangeiras. Atendimento continua dependendo de Agendamento, e por isso recebe a FK agendamento_id.
É aqui que UML mostra sua força pedagógica e prática. Se o modelo conceitual estiver mal pensado, o banco costuma denunciar isso rapidamente. Se uma associação estiver ambígua, você não saberá onde colocar a FK. Se uma cardinalidade estiver errada, a estrutura relacional ficará inconsistente.
Como isso volta para o código C++6.7.5
Em um sistema real, o código não trabalha apenas com classes de domínio. Ele também precisa de repositórios, consultas e mapeamento entre objetos e linhas do banco. Mesmo sem implementar SQL aqui, já podemos visualizar como o objeto Agendamento corresponde a uma linha na tabela agendamentos.
struct AgendamentoRecord {
int id;
int petId;
int servicoId;
int funcionarioId;
std::string dataHora;
std::string status;
};
class AgendaRepository {
public:
int salvar(const Agendamento& agendamento);
std::optional<AgendamentoRecord> buscarPorId(int id) const;
};
Aqui você já consegue enxergar duas camadas convivendo.
- a camada de domínio, com classes como
Agendamento - a camada de persistência, com estruturas e operações voltadas ao banco
Em projetos maiores, essa separação precisa ser tratada com bastante cuidado. Em aulas introdutórias, o mais importante é compreender que o banco não substitui o modelo de domínio. Ele o armazena.
Um refinamento importante: e se um agendamento tiver vários serviços?6.7.6
Até aqui simplificamos o sistema dizendo que cada Agendamento possui exatamente um Servico. Isso foi útil para ensinar o básico com clareza. Mas imagine agora que o pet faça banho e tosa no mesmo horário. Nesse caso, a relação deixa de ser 1 para 1 e passa a ser muitos para muitos entre Agendamento e Servico.
No Diagrama de Classes, isso exigiria revisar a cardinalidade. No banco, isso exigiria uma tabela associativa, por exemplo agendamento_servicos.
Esse exemplo mostra por que a modelagem é iterativa. Você raramente acerta todos os detalhes de primeira. O valor da UML está justamente em permitir esse refinamento antes de o custo da mudança ficar alto demais.
Fechando a evolução do modelo6.8
Vamos recapitular a trajetória completa do nosso único domínio.
Começamos com a pergunta mais externa possível. O que o sistema do pet shop precisa oferecer? Isso nos levou ao Diagrama de Casos de Uso. Em seguida perguntamos quais entidades do domínio precisam existir para sustentar essas funcionalidades. Isso nos levou ao Diagrama de Classes. Depois verificamos como esses objetos colaboram em um cenário concreto de agendamento, usando o Diagrama de Sequência. Na sequência, observamos o fluxo operacional completo com decisões e desvios no Diagrama de Atividades. Por fim, traduzimos a estrutura do domínio para um esquema relacional por meio do diagrama ER.
Essa progressão é importante porque espelha a própria lógica do desenvolvimento de software orientado a objetos.
- primeiro você entende o problema
- depois organiza o domínio
- depois valida o comportamento
- depois pensa na persistência
Se você inverter isso e começar pelo banco ou por classes improvisadas, corre o risco de construir uma implementação tecnicamente válida, mas conceitualmente ruim.
UML não substitui o código, mas melhora muito a qualidade do código que você vai escrever. Ela reduz ambiguidade, explicita responsabilidades e cria uma ponte clara entre requisito, estrutura orientada a objetos e persistência relacional.
Questões6.9
1. Explique, com suas palavras, por que o Diagrama de Casos de Uso foi o primeiro da sequência desta aula. O que ele mostra que os outros ainda não mostram?
2. No nosso modelo do pet shop, por que foi útil separar Agendamento de Atendimento? Explique o ganho conceitual dessa decisão para o código e para o banco de dados.
3. No Diagrama de Classes, a relação entre Cliente e Pet foi modelada como 1 para 0..*. Mostre como essa cardinalidade aparece tanto no código C++ quanto no banco de dados relacional.
4. Compare o papel do Diagrama de Sequência com o papel do Diagrama de Atividades usando o cenário de agendamento do pet shop. Qual pergunta cada um responde melhor?
5. No diagrama ER, a tabela agendamentos possui chaves estrangeiras para pets, servicos e funcionarios. Explique por que isso é uma consequência direta do Diagrama de Classes e não uma decisão independente.
6. Suponha que o pet shop decida permitir mais de um serviço no mesmo agendamento. O que precisaria mudar no Diagrama de Classes, no banco de dados e possivelmente no código C++?
7. Considere a regra operacional "se o pet não comparecer, o agendamento deve ser cancelado". Em qual diagrama essa regra fica mais clara de visualizar? Em qual ponto ela provavelmente apareceria no código?
1. Porque o Diagrama de Casos de Uso mostra a visão externa do sistema e organiza as funcionalidades do ponto de vista dos atores. Antes de pensar em classes, métodos e banco, precisávamos entender quais serviços o sistema realmente oferece e qual é o seu escopo.
2. Separar Agendamento de Atendimento evita misturar planejamento com execução. Agendamento representa a marcação futura do serviço. Atendimento representa a realização concreta, com início, fim, observações e valor cobrado. No código, isso reduz acoplamento e excesso de responsabilidade em uma única classe. No banco, evita atributos nulos ou sem sentido antes da execução do serviço.
3. No código C++, a cardinalidade aparece por meio de uma coleção, como std::vector<std::shared_ptr<Pet>>, dentro de Cliente. No banco, aparece com uma chave estrangeira cliente_id na tabela pets, permitindo que vários pets apontem para um mesmo cliente.
4. O Diagrama de Sequência responde melhor à pergunta "em que ordem os objetos e componentes trocam mensagens para realizar o agendamento?". O Diagrama de Atividades responde melhor à pergunta "quais etapas e decisões compõem o processo de agendamento e atendimento?". O primeiro enfatiza colaboração entre participantes. O segundo enfatiza fluxo e decisão.
5. Porque o Diagrama de Classes já estabeleceu que Agendamento se associa a Pet, Servico e Funcionario. Ao mapear esse modelo para o banco, essas associações precisam ser preservadas. No modelo relacional, isso normalmente ocorre por meio de chaves estrangeiras nas tabelas correspondentes.
6. No Diagrama de Classes, a associação entre Agendamento e Servico deixaria de ser 1 para 1 e passaria a ser muitos para muitos ou 1 para 1..*, dependendo do refinamento escolhido. No banco, seria necessário criar uma tabela associativa, como agendamento_servicos. No código C++, Agendamento provavelmente deixaria de guardar um único Servico e passaria a manter uma coleção, como std::vector<std::shared_ptr<Servico>>.
7. A regra fica mais clara no Diagrama de Atividades, porque ele destaca o ponto de decisão Pet compareceu? e o desvio para cancelamento. No código, ela provavelmente apareceria em uma rotina de confirmação de chegada ou em um serviço de aplicação que atualiza o status do agendamento.
Próximos passos6.10
Agora que você modelou o sistema do ponto de vista funcional, estrutural, comportamental e relacional, o próximo avanço natural é estudar como organizar essa solução em uma arquitetura de software mais ampla. Na próxima aula, avançaremos para Padrões de Arquiteturas, com foco em exemplos como MVC, para entender como os elementos que você modelou aqui se distribuem em camadas, componentes e responsabilidades de implementação.