Diagrama de classes

Gato engenheiro de software que está criando um projeto de software em UML
Gato engenheiro de software que está criando um projeto de software em UML.
Prompt: crie uma imagem de um gato engenheiro de software usando óculos criando um diagrama de classes UML. O gato deve estar usando roupas formais.
Fonte: gerado por IA com Bing por Maxwell Anderson (2023)

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.

Relacionamento entre casos de uso e casos de uso com generalização/especialização

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
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 classe com associação binária com multiplicidade
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
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
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
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
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
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
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 classe com associação binária com multiplicidade
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
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
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
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
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
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:

Exemplo de interface 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