Diagrama de classes
Nota
Para compreender esta lição, é necessário ter conhecimento prévio sobre o paradigma de programação orientada a objetos. Se você não tem conhecimento sobre o assunto, recomendo que estude antes de prosseguir.
Introdução
O diagrama de classes é um dos mais utilizados e importantes da UML. Permite visualizar as classes que irão compor o sistema.
Veja o caso de uso Abrir conta comum
da lição anterior. Nele, temos que o ator Cliente
irá interagir com o sistema para realizar a abertura de uma conta comum.
Durante o processo de análise, um caso de uso poderá gerar uma classe. Desta forma, teremos:
Exemplo de classe com atributos privados e métodos públicos
Fonte: elaboração própria (2023)
Elementos
Um classe é desenhada como um retângulo com três divisões, contendo:
- Nome da classe
- Atributos
- Métodos
Atributos e métodos
Veja novamente a classe acima. Os atributos são escritos com o nome do atributo seguido de :
e o tipo do atributo.
Se não existirem atributos, então o retângulo que contém os atributos terá espaço vazio.
Os métodos são escritos com o nome do método seguido de ()
e o tipo de retorno do método. Os métodos:
- sem retorno são escritos com
void
. - sem parâmetros possuem
()
vazio. - com parâmetros são escritos na forma
(nome_atributo1: tipoA, nome_atributo2: tipoB, ..., nome_atributoN: tipoC)
.
Se não existirem métodos, então a divisão de métodos não é desenhada.
Nota
Se você assume o papel de analista de sistemas e irá necessitar modelar um sistema, ao término do seu trabalho, esses diagramas serão enviados aos desenvolvedores em forma de projeto. Os desenvolvedores irão implementar o sistema com base nos diagramas que você criou.
Lembre-se também que as classes foram desenvolvidas a partir dos casos de usos que o analista também criou anteriormente.
Faz sentido para você essa lógica de trabalho?
Assim, para o desenvolvedor, como ele criaria o código Java para a classe acima?
class Conta {
private Date data_abertura;
private Date data_encerramento;
private String numero_conta;
private double saldo;
private String senha;
private int situacao;
public void abrirConta(String conta) {
// Implementação do método aqui
}
public Bool consultarConta(String agencia, String conta) {
// Implementação do método aqui
}
public float saldoConta(String agencia, String conta) {
// Implementação do método aqui
}
public Bool validarSenha() {
// Implementação do método aqui
}
}
E como seria a implementação em Python?
class Conta:
def __init__(self):
self.data_abertura: datetime = None
self.data_encerramento: datetime = None
self.numero_conta: str = ''
self.saldo: float = 0.0
self.senha: str = ''
self.situacao: int = 0
def abrirConta(self, conta: str) -> None:
# Implementação do método aqui
def consultarConta(self, agencia: str, conta: str) -> bool:
# Implementação do método aqui
def saldoConta(self, agencia: str, conta: str) -> float:
# Implementação do método aqui
def validarSenha(self) -> bool:
# Implementação do método aqui
Nota para desenvolvedores
Perceba que as anotações de tipo são apenas sugestões e não forçam o Python a usar esses tipos. Se você atribuir um valor de um tipo diferente a um desses atributos, o Python não irá parar você. As anotações de tipo servem principalmente para documentação e para ferramentas de análise de código.
Visibilidade
Os atributos e métodos podem ser públicos, privados ou protegidos. Lembra das aulas de programação orientada a objetos?
- Público: pode ser acessado por qualquer classe.
- Privado: só pode ser acessado pela própria classe.
- Protegido: só pode ser acessado pela própria classe e pelas classes filhas.
A visibilidade é representada por um símbolo antes do nome do atributo ou método:
+
para público-
para privado#
para protegido
Assim, temos:
Exemplo de diagrama de classes com elementos privados, públicos e protegidos
Fonte: elaboração própria (2023)
Assim, interpretando o diagrama acima, temos que ContaEspecial
não “enxerga” o atributo saldo
da classe ContaComum
, pois ele é privado. Todavia, ContaEspecial
pode acessar o método abrirConta()
da classe ContaComum
, pois ele é público. O mesmo vale para o atributo senha
, pois ele é protegido.
Relacionamentos ou associações
Associações são relacionamentos entre classes. São representadas por uma linha que liga duas classes. Podem ser:
- Unária ou reflexiva
- Binária
- Binária com multiplicidade
- Generalização e especialização
- Agregação
- Composição
Associação unária ou reflexiva
Temos abaixo um exemplo de classe com associação unária ou reflexiva:
Exemplo de classe com associação unária ou reflexiva
Fonte: elaboração própria (2023)
Veja que a associação reflexiva é uma associação entre objetos da mesma classe. No exemplo acima, temos que um funcionário pode ter nenhum ou vários colegas. Desta forma, podemos podemos visualizar como ficaria o diagrama de classes e o código relacionado:
ℹ️ Nota
Não se concentre em interpretar todo o código, mas se concentre como as relações entre as classes são implementadas.
Quem vai ter que se preocupar em implementar as classes são os desenvolvedores. A não ser que você é o cara que faz tudo. 😂
import java.util.List;
import java.util.ArrayList;
class Funcionario {
private String nome;
private String cargo;
private List<Funcionario> colegasDeTrabalho;
// Construtor
public Funcionario(String nome, String cargo) {
this.nome = nome;
this.cargo = cargo;
this.colegasDeTrabalho = new ArrayList<>();
}
// Métodos de acesso para os atributos privados
public String getNome() {
return nome;
}
public String getCargo() {
return cargo;
}
// Método para adicionar um colega de trabalho
public void adicionarColega(Funcionario colega) {
colegasDeTrabalho.add(colega);
colega.colegasDeTrabalho.add(this); // Estabelece a associação bidirecional
}
// Método para obter a lista de colegas de trabalho
public List<Funcionario> getColegasDeTrabalho() {
return colegasDeTrabalho;
}
}
Em Python, o código ficaria assim:
class Funcionario:
def __init__(self, nome, cargo):
self.nome = nome
self.cargo = cargo
self.colegas_de_trabalho = []
# Método para adicionar um colega de trabalho
def adicionar_colega(self, colega):
self.colegas_de_trabalho.append(colega)
colega.colegas_de_trabalho.append(self) # Estabelece a associação bidirecional
Exemplo de uso:
funcionario1 = Funcionario("Alice", "Gerente")
funcionario2 = Funcionario("Bob", "Analista")
funcionario3 = Funcionario("Charlie", "Desenvolvedor")
funcionario1.adicionar_colega(funcionario2)
funcionario1.adicionar_colega(funcionario3)
# Obtendo os colegas de trabalho de funcionario1
for colega in funcionario1.colegas_de_trabalho:
print(colega.nome, colega.cargo)
Associação binária
Genericamente, aqui podemos visualizar como ficaria o diagrama de classes e o código relacionado:
Exemplo de diagrama de classes com associação binária
Fonte: elaboração própria (2023)
A leitura da associação deveria ser feita da sequinte forma: “uma instância da classe A possui uma instância da classe B”.
class A {
private B b;
}
class B {
}
Temos abaixo um exemplo de classe com associação binária:
Exemplo de classe com associação binária
Fonte: elaboração própria (2023)
Na associação binária, temos que um sócio pode possuir vários dependentes. Este possui uma seta para indicar a navegabilidade. Ou seja, um sócio pode acessar seus dependentes, mas um dependente não pode acessar seu sócio. Assim a navegabilidade indica o sentido da associação.
Segue exemplos de código em Java para as classes Socio e Dependente:
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
class Socio {
private Date data;
private String endereco;
private String nome;
private String telefone;
private List<Dependente> dependentes;
public Socio(Date data, String endereco, String nome, String telefone) {
this.data = data;
this.endereco = endereco;
this.nome = nome;
this.telefone = telefone;
this.dependentes = new ArrayList<>();
}
public void adicionarDependente(Dependente dependente) {
dependentes.add(dependente);
}
public List<Dependente> getDependentes() {
return dependentes;
}
}
class Dependente {
private Date data;
private String nome;
public Dependente(Date data, String nome) {
this.data = data;
this.nome = nome;
}
}
Em Python, o código ficaria assim:
from datetime import date
class Socio:
def __init__(self, data, endereco, nome, telefone):
self.data = data
self.endereco = endereco
self.nome = nome
self.telefone = telefone
self.dependentes = []
def adicionar_dependente(self, dependente):
self.dependentes.append(dependente)
def get_dependentes(self):
return self.dependentes
class Dependente:
def __init__(self, data, nome):
self.data = data
self.nome = nome
Exemplo de uso em Python
socio1 = Socio(date(1990, 1, 15), "Rua A, 123", "João", "123-456-789")
dependente1 = Dependente(date(2010, 5, 20), "Maria")
dependente2 = Dependente(date(2012, 8, 10), "Pedro")
socio1.adicionar_dependente(dependente1)
socio1.adicionar_dependente(dependente2)
# Obtendo os dependentes do sócio
for dependente in socio1.get_dependentes():
print("Dependente:", dependente.nome, "- Data de Nascimento:", dependente.data)
Segue um outro exemplo de aplicação de associação binária, porém com uso de atributo privado:
Aplicação de associação binária com uso de atributo privado
Fonte: elaboração própria (2023)
Veja que o atributo fone
é privado e recebe um único valor.
O código em Java:
class Fone {
private String codigo;
private String numero;
private boolean celular;
public void setFone(String codigo, String numero, boolean celular) {
this.codigo = codigo;
this.numero = numero;
this.celular = celular;
}
public String getFone() {
return codigo + numero;
}
public boolean isCelular() {
return celular;
}
}
class Pessoa {
private String nome;
private String sobrenome;
private Fone fone;
public void setPessoa(String nome, String sobrenome, Fone fone) {
this.nome = nome;
this.sobrenome = sobrenome;
this.fone = fone;
}
public Pessoa getPessoa() {
return this;
}
}
Em Python, o código ficaria assim:
class Fone:
def __init__(self):
self.codigo = ""
self.numero = ""
self.celular = False
def setFone(self, codigo, numero, celular):
self.codigo = codigo
self.numero = numero
self.celular = celular
def getFone(self):
return self.codigo + self.numero
def isCelular(self):
return self.celular
class Pessoa:
def __init__(self):
self.nome = ""
self.sobrenome = ""
self.fone = Fone()
def setPessoa(self, nome, sobrenome, fone):
self.nome = nome
self.sobrenome = sobrenome
self.fone = fone
def getPessoa(self):
return self
Exemplo de uso em Python
fone = Fone()
fone.setFone("55", "123456789", True)
pessoa = Pessoa()
pessoa.setPessoa("Alice", "Silva", fone)
# Obtendo informações da pessoa e seu telefone
print("Nome:", pessoa.nome, pessoa.sobrenome)
print("Telefone:", pessoa.fone.getFone())
print("É celular?", pessoa.fone.isCelular())
Associação binária com multiplicidade
Temos abaixo um exemplo de classe com associação binária com multiplicidade:
Exemplo de classe com associação binária com multiplicidade
Fonte: elaboração própria (2023)
Em 0..1
temos que uma pessoa pode não ter nenhum telefone (0
) ou até um telefone (1
). Neste caso, o desenvolvedor terá que implementar a restrição sobre a quantidade de objetos que podem ser criados (nenhum telefone ou um único telefone).
Outros exemplos poderiam ser:
- Em
0..*
temos que uma pessoa pode não ter nenhum telefone (0
) ou até vários telefones (*
). - Em
1..*
temos que uma pessoa pode ter pelo menos um telefone (1
) ou até vários telefones (*
).
Um quadro geral para representar a multiplicidade é:
Nome | Simbologia | Descrição |
---|---|---|
Um | 1 |
Um único objeto |
Vários | * |
Vários objetos |
Um ou vários | 1..* |
Um ou vários objetos |
Nenhum ou um | 0..1 |
Nenhum ou um único objeto |
Nenhum ou vários | 0..* |
Nenhum ou vários objetos |
No primeiro caso, o código ficaria assim:
class Pessoa {
private Fone[] fone;
}
class Fone {
}
O segundo caso, onde um telefone poderá ser vinculado a várias pessoas:
Exemplo de classe com associação binária com multiplicidade
Fonte: elaboração própria (2023)
Segue o código:
class Pessoa {
private Fone[] fone;
}
class Fone {
private Pessoa[] dono;
}
“Oxente! Mas se uma pessoa pode ter nenhum ou um único telefone e um telefone pode ser vinculado a várias pessoas, por que o código aparenta ser uma relação muitos para muitos?”
Porque o desenvolvedor terá que implementar “na mão” a restrição sobre a quantidade de objetos que podem ser criados (nenhum telefone ou um único telefone). Ele vai dar um jeito de implementar isso. 😁
ℹ️ Nota
Um telefone vinculado a várias pessoas? Consegue imaginar um exemplo de aplicação? ;)
❔Você sabia?
Que podemos usar um diagrama de classes UML para representar entidades de dados ao invés de um Diagrama Entidade-Relacionamento (DER) visto por vocês na disciplina de Banco de Dados? Pois é! Perceba que na UML não existe um diagrama específico para representar entidades de dados. E como podemos fazer isso? Simples! Basta usar a associação binária com multiplicidade.
Generalização e especialização
A generalização/especialização de classes é um relacionamento entre classes tal como visto entre atores e atores na lição sobre Diagramas de Casos de Uso. Desta forma, podemos ter:
Exemplo de diagrama de classes com generalização/especialização
Fonte: elaboração própria (2023)
O código em Java ficará assim:
import java.util.Date;
class ContaComum {
private Date dataAbertura;
private Date dataEncerramento;
private String numeroConta;
private float saldo;
private String senha;
private int situacao;
public void abrirConta(String conta) {
// Lógica para abrir uma conta
}
public boolean consultarConta(String agencia, String conta) {
// Lógica para consultar uma conta
return true;
}
public float saldoConta(String agencia, String conta) {
// Lógica para verificar o saldo de uma conta
return saldo;
}
public boolean validarSenha() {
// Lógica para validar a senha
return true;
}
}
class ContaEspecial extends ContaComum {
private double limite;
}
class ContaPoupanca extends ContaComum {
private Date dataAniversario;
}
Já em Python:
from datetime import date
class ContaComum:
def __init__(self):
self.data_abertura = date.today()
self.data_encerramento = None
self.numero_conta = ""
self.saldo = 0.0
self.senha = ""
self.situacao = 0
def abrir_conta(self, conta):
# Lógica para abrir uma conta
pass
def consultar_conta(self, agencia, conta):
# Lógica para consultar uma conta
return True
def saldo_conta(self, agencia, conta):
# Lógica para verificar o saldo de uma conta
return self.saldo
def validar_senha(self):
# Lógica para validar a senha
return True
class ContaEspecial(ContaComum):
def __init__(self):
super().__init__()
self.limite = 0.0
class ContaPoupanca(ContaComum):
def __init__(self):
super().__init__()
self.data_aniversario = date.today()
Exemplo de uso em Python:
conta_especial = ContaEspecial()
conta_especial.abrir_conta("123456")
conta_poupanca = ContaPoupanca()
conta_poupanca.abrir_conta("789012")
print("Conta Especial:")
print("Saldo:", conta_especial.saldo_conta("agencia", conta_especial.numero_conta))
print("Conta Poupança:")
print("Saldo:", conta_poupanca.saldo_conta("agencia", conta_poupanca.numero_conta))
Agregação
A agregação é um tipo de associação onde um objeto é composto por outros objetos. Guardam uma relação todo-parte entre si.
Como assim? Pense na seguinte situação: se um jogador faz parte de uma equipe, isso pode ser considerado uma relação de associação? Sim, sim! E não só uma associação simples, mas uma agregação. O jogador é um “agregado” da equipe. Ou seja, a equipe é composta por jogadores.
Com isso, existe uma regra para verificar a relação de agregação entre duas classes. Se uma das perguntas abaixo for verdadeira, então temos uma agregação:
- B é parte de A?
- A tem um ou mais B?
Exemplo de classe com agregação
Fonte: elaboração própria (2023)
A agregação é representada por uma linha com um losango vazado na ponta que aponta para a classe que representa o todo.
Neste exemplo, Jogador
é a parte e Equipe
é o todo.
Cada equipe possui um ou mais jogadores. E cada jogador faz parte de uma equipe.
- Se a equipe deixar de existir, os jogadores poderão fazer parte de outras equipes.
- Se um jogador deixar de existir, a equipe continuará existindo.
- Um jogador também poderá fazer parte de mais de uma equipe.
A ideia é que se a equipe deixar de existir, ninguém vai ser demitido! 😁
Composição
A composição é um tipo de agregação mais forte.
Aqui a história muda! Se a equipe deixar de existir, os jogadores serão demitidos! 😱
Assim, os objetos-parte (jogadores) não podem existir sem o objeto-todo (equipe). Os objetos-parte são sempre criados e destruídos pelo objeto-todo. Se o todo deixa de existir, as partes também deixarão de existir.
Exemplo de classe com composição
Fonte: elaboração própria (2023)
A composição é representada por uma linha com um losango preenchido na ponta que aponta para a classe que representa o todo.
Exemplo completo
Veja um exemplo completo de diagrama de classes com todas as associações apresentadas até aqui.
Exemplo de diagrama completo
Fonte: elaboração própria (2023)
Quando eu devo usar uma simples associação, uma relação de agregação ou de composição? Tudo depende da análise subjetiva do analista de sistemas. Se vai usar multiplicidade ou não, se vai usar associação unária ou binária etc.
Assim podemos interpretar o diagrama:
- Uma Associação possui nenhuma ou uma Equipe. Equipe só existirá se existir uma Associação (temos uma relação todo-parte forte aqui).
- Uma Associação possui um ou vários Funcionários.
- Um Funcionário supervisiona nenhum ou vários Funcionários.
- Um Funcionário é supervisionado por um Funcionário.
- Uma Equipe possui de 11 a 22 Jogadores (temos uma relação todo-parte fraca, pois um jogador poderá existir caso uma equipe não exista).
- Uma Equipe possui um ou dois Técnicos (o mesmo acima).
- Jogador e Técnico são tipos de Membros.
Viu um “A” na classe Membro
, inclusive com nome escrito em itálico? Isso significa que a classe é abstrata. Não pode ser instanciada. Ela só existe para ser herdada por outras classes. Vamos ver isso a seguir.
Classes e métodos abstratos
Uma classe abstrata é uma classe que não pode ser instanciada. É utilizada apenas para ser herdada por outras classes.
Classes abstratas só existem para serem herdadas.
É representada por um nome em itálico.
Exemplo de classe abstrata
Fonte: elaboração própria (2023)
Um método abstrato é um método que não possui implementação. Ele é usado apenas para ser sobrescrito por outras classes.
O método abstrato é representado por um nome em itálico, assim como a classe.
Interfaces
Uma interface descreve um conjunto de serviços fornecidos. É muito utilizada para diagramar APIs. Define um “contrato” que deve ser implementado por outras classes.
A interface é representada por um nome em itálico e com o nome precedido pelo estereótipo <<interface>>
ou símbolo correspondente. É comum que o nome da interface comece com a letra “I” maiúscula.
Exemplo de interface
Fonte: elaboração própria (2023)
Aqui as interfaces são especificadas de maneira detalhada.
Se não for necessário especificar os métodos, então podemos usar a notação simplificada:
Referências
—. Aula 03 UML Parte01. Universidade Salvador.
Guedes, G. T. A. UML 2 Uma abordagem prática. 1ª edição. São Paulo: Novatec Editora, 2009.
Marco Tulio Valente. Engenharia de Software Moderna: Princípios e Práticas para Desenvolvimento de Software com Produtividade, Editora: Independente, 395 páginas, 2020.
Pressman, S. R. Engenharia de Software. 6ª edição. São Paulo: McGraw-Hill, 2006.
Tonsig, S. L. Engenharia de Software. Análise e Projeto de Sistemas. 1ª edição. São Paulo: Futura, 2003.
Criado em Junho de 2023 por Maxwell Anderson