O que são invariantes?
Invariantes são regras ou condições que devem ser sempre verdadeiras para um objeto ou sistema, independentemente das operações realizadas. Por exemplo:
- Em uma classe
Usuario
, um invariante pode ser que o nome não seja nulo e tenha entre 2 e 100 caracteres. - Outro invariante pode ser que a idade seja maior ou igual a 18.
Proteger as invariantes é essencial para garantir a consistência e a integridade dos dados em um sistema.
Jakarta Validation e Invariantes
O Jakarta Validation (anteriormente Bean Validation) é uma especificação que permite definir regras de validação usando anotações como @NotNull
, @Size
, @Email
, @Min
, etc. Ele é amplamente usado para garantir que os dados respeitem as invariantes.
Como o Jakarta Validation ajuda?
- Validação declarativa: Você define as regras diretamente nos campos ou métodos da classe usando anotações.
- Integração com frameworks: Frameworks como Spring, Quarkus e Jakarta EE integram automaticamente o Jakarta Validation, validando os dados em pontos específicos do ciclo de vida da aplicação (como em APIs REST ou ao persistir dados com JPA).
-
Customização: Você pode criar validadores personalizados usando a interface
ConstraintValidator
.
Garantindo que um objeto seja criado em estado consistente
Para garantir que um objeto da classe Usuario
seja criado em um estado consistente, você pode adotar as seguintes práticas:
1. Usar um construtor que valide as invariantes
O construtor deve garantir que os valores passados respeitem as invariantes. Se algum valor for inválido, o construtor deve lançar uma exceção. Aqui, podemos usar a classe Assert
para simplificar as validações.
Exemplo com Assert
:
import org.springframework.util.Assert;
public class Usuario {
private final String nome;
private final String email;
private final int idade;
public Usuario(String nome, String email, int idade) {
// Validações usando Assert
Assert.notNull(nome, "O nome não pode ser nulo.");
Assert.isTrue(nome.length() >= 2 && nome.length() <= 100, "O nome deve ter entre 2 e 100 caracteres.");
Assert.notNull(email, "O email não pode ser nulo.");
Assert.isTrue(email.matches("^[A-Za-z0-9+_.-]+@(.+)$"), "O email deve ser válido.");
Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");
this.nome = nome;
this.email = email;
this.idade = idade;
}
// Getters (sem setters para imutabilidade)
public String getNome() {
return nome;
}
public String getEmail() {
return email;
}
public int getIdade() {
return idade;
}
}
Vantagens:
- Código mais limpo e expressivo.
- Lança exceções específicas (
IllegalArgumentException
) com mensagens claras. - Ideal para validações simples e diretas.
Desvantagens:
- Depende de uma biblioteca externa (como Spring). Se não estiver usando Spring, você pode criar sua própria classe
Assert
.
2. Usar o padrão Builder com Assert
O padrão Builder é útil quando você tem muitas propriedades para validar ou quer fornecer uma API mais fluente para a criação de objetos. Podemos usar Assert
para validar cada propriedade.
Exemplo:
import org.springframework.util.Assert;
public class Usuario {
private final String nome;
private final String email;
private final int idade;
private Usuario(Builder builder) {
this.nome = builder.nome;
this.email = builder.email;
this.idade = builder.idade;
}
// Getters
public String getNome() {
return nome;
}
public String getEmail() {
return email;
}
public int getIdade() {
return idade;
}
// Builder
public static class Builder {
private String nome;
private String email;
private int idade;
public Builder nome(String nome) {
Assert.notNull(nome, "O nome não pode ser nulo.");
Assert.isTrue(nome.length() >= 2 && nome.length() <= 100, "O nome deve ter entre 2 e 100 caracteres.");
this.nome = nome;
return this;
}
public Builder email(String email) {
Assert.notNull(email, "O email não pode ser nulo.");
Assert.isTrue(email.matches("^[A-Za-z0-9+_.-]+@(.+)$"), "O email deve ser válido.");
this.email = email;
return this;
}
public Builder idade(int idade) {
Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");
this.idade = idade;
return this;
}
public Usuario build() {
return new Usuario(this);
}
}
}
Uso:
Usuario usuario = new Usuario.Builder()
.nome("João Silva")
.email("joao.silva@example.com")
.idade(25)
.build();
Vantagens:
- Flexibilidade para definir propriedades de forma fluente.
- Validação feita em cada etapa, garantindo consistência.
3. Usar Jakarta Validation no construtor
Se você estiver usando o Jakarta Validation, pode validar o objeto após a criação no construtor. Aqui, podemos combinar Assert
com o Jakarta Validation para garantir consistência.
Exemplo:
import jakarta.validation.*;
import jakarta.validation.constraints.*;
import org.springframework.util.Assert;
import java.util.Set;
public class Usuario {
@NotNull(message = "O nome não pode ser nulo")
@Size(min = 2, max = 100, message = "O nome deve ter entre 2 e 100 caracteres")
private final String nome;
@NotNull(message = "O email não pode ser nulo")
@Email(message = "O email deve ser válido")
private final String email;
@Min(value = 18, message = "A idade deve ser maior ou igual a 18")
private final int idade;
public Usuario(String nome, String email, int idade) {
// Validações simples com Assert
Assert.notNull(nome, "O nome não pode ser nulo.");
Assert.notNull(email, "O email não pode ser nulo.");
Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");
this.nome = nome;
this.email = email;
this.idade = idade;
// Validação com Jakarta Validation
validar();
}
private void validar() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Usuario>> violations = validator.validate(this);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder("Erros de validação:\n");
for (ConstraintViolation<Usuario> violation : violations) {
sb.append("- ").append(violation.getMessage()).append("\n");
}
throw new IllegalArgumentException(sb.toString());
}
}
// Getters
public String getNome() {
return nome;
}
public String getEmail() {
return email;
}
public int getIdade() {
return idade;
}
}
Vantagens:
- Combina validações simples com
Assert
e validações complexas com Jakarta Validation. - Centraliza a validação em um único método.
4. Imutabilidade
Tornar a classe Usuario
imutável (sem métodos setters) garante que, uma vez criado, o objeto não possa ser alterado para um estado inconsistente.
Exemplo:
public class Usuario {
private final String nome;
private final String email;
private final int idade;
public Usuario(String nome, String email, int idade) {
// Validações com Assert
Assert.notNull(nome, "O nome não pode ser nulo.");
Assert.isTrue(nome.length() >= 2 && nome.length() <= 100, "O nome deve ter entre 2 e 100 caracteres.");
Assert.notNull(email, "O email não pode ser nulo.");
Assert.isTrue(email.matches("^[A-Za-z0-9+_.-]+@(.+)$"), "O email deve ser válido.");
Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");
this.nome = nome;
this.email = email;
this.idade = idade;
}
// Getters (sem setters para imutabilidade)
public String getNome() {
return nome;
}
public String getEmail() {
return email;
}
public int getIdade() {
return idade;
}
}
Vantagens:
- Evita que o objeto seja modificado após a criação.
- Garante consistência durante todo o ciclo de vida do objeto.
Quando a classe Usuario
é uma @Entity
(ou seja, uma entidade JPA), há considerações adicionais a serem feitas para garantir que as invariantes sejam protegidas e que a validação seja integrada ao ciclo de vida da persistência. Vou adaptar as práticas anteriores para o contexto de uma entidade JPA, incluindo o uso de Assert
, Jakarta Validation, e a integração com o ciclo de vida do JPA.
Protegendo Invariantes em uma Entidade JPA
Considerações Importantes
-
Validação Automática com JPA:
- O JPA (Java Persistence API) pode integrar-se automaticamente com o Jakarta Validation para validar entidades antes de persistir no banco de dados.
- Se uma entidade violar as regras de validação, o JPA lançará uma exceção (
ConstraintViolationException
).
-
Ciclo de Vida da Entidade:
- A validação ocorre automaticamente durante operações como
persist
,merge
, eremove
. - Você pode usar o
@PrePersist
e@PreUpdate
para adicionar validações personalizadas.
- A validação ocorre automaticamente durante operações como
-
Uso de
Assert
:- A classe
Assert
pode ser usada para validações programáticas dentro dos métodos da entidade.
- A classe
-
Imutabilidade:
- Em entidades JPA, a imutabilidade total nem sempre é prática, pois o JPA requer métodos setters para atualizar o estado. No entanto, você pode proteger campos críticos (como IDs) para evitar alterações inconsistentes.
Exemplo Completo: Classe Usuario
como @Entity
Aqui está um exemplo completo de uma classe Usuario
como entidade JPA, com validações usando Jakarta Validation, Assert
, e métodos de ciclo de vida.
Código:
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import org.springframework.util.Assert;
import java.util.Objects;
@Entity
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull(message = "O nome não pode ser nulo")
@Size(min = 2, max = 100, message = "O nome deve ter entre 2 e 100 caracteres")
@Column(nullable = false, length = 100)
private String nome;
@NotNull(message = "O email não pode ser nulo")
@Email(message = "O email deve ser válido")
@Column(nullable = false, unique = true, length = 100)
private String email;
@Min(value = 18, message = "A idade deve ser maior ou igual a 18")
@Column(nullable = false)
private int idade;
// Construtor padrão (obrigatório para JPA)
protected Usuario() {}
// Construtor principal
public Usuario(String nome, String email, int idade) {
this.nome = nome;
this.email = email;
this.idade = idade;
validar(); // Validação manual
}
// Getters e Setters (obrigatórios para JPA)
public Long getId() {
return id;
}
protected void setId(Long id) { // Setter protegido para evitar alterações externas
this.id = id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
Assert.notNull(nome, "O nome não pode ser nulo.");
Assert.isTrue(nome.length() >= 2 && nome.length() <= 100, "O nome deve ter entre 2 e 100 caracteres.");
this.nome = nome;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
Assert.notNull(email, "O email não pode ser nulo.");
Assert.isTrue(email.matches("^[A-Za-z0-9+_.-]+@(.+)$"), "O email deve ser válido.");
this.email = email;
}
public int getIdade() {
return idade;
}
public void setIdade(int idade) {
Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");
this.idade = idade;
}
// Método de validação manual
private void validar() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
var violations = validator.validate(this);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder("Erros de validação:\n");
for (var violation : violations) {
sb.append("- ").append(violation.getMessage()).append("\n");
}
throw new IllegalArgumentException(sb.toString());
}
}
// Métodos de ciclo de vida (opcional)
@PrePersist
@PreUpdate
private void antesDePersistirOuAtualizar() {
validar(); // Validação automática antes de persistir ou atualizar
}
// Equals e HashCode (opcional, mas recomendado para entidades)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Usuario usuario = (Usuario) o;
return Objects.equals(id, usuario.id); // Compara apenas o ID
}
@Override
public int hashCode() {
return Objects.hash(id); // Usa apenas o ID
}
@Override
public String toString() {
return "Usuario{" +
"id=" + id +
", nome='" + nome + '\'' +
", email='" + email + '\'' +
", idade=" + idade +
'}';
}
}
Explicação dos Pontos-Chave
-
Validação com Jakarta Validation:
- As anotações como
@NotNull
,@Size
,@Email
, e@Min
são usadas para definir as regras de validação. - O JPA valida automaticamente a entidade antes de persistir ou atualizar no banco de dados.
- As anotações como
-
Uso de
Assert
:- A classe
Assert
é usada nos métodos setters para garantir que os valores sejam válidos antes de atribuí-los aos campos.
- A classe
-
Métodos de Ciclo de Vida:
- O método
@PrePersist
e@PreUpdate
é usado para chamar a validação manual antes de persistir ou atualizar a entidade.
- O método
-
Imutabilidade Parcial:
- O campo
id
tem um setter protegido para evitar alterações externas. - Os setters públicos validam os dados antes de atribuí-los.
- O campo
-
Validação Manual:
- O método
validar()
usa oValidator
do Jakarta Validation para verificar as anotações e lançar exceções em caso de violações.
- O método
-
Equals e HashCode:
- São implementados com base no campo
id
, que é a chave primária da entidade.
- São implementados com base no campo
Como Funciona na Prática
Criando um Usuário Válido:
Usuario usuario = new Usuario("João Silva", "joao.silva@example.com", 25);
// O objeto é criado e validado com sucesso.
Tentando Criar um Usuário Inválido:
try {
Usuario usuario = new Usuario(null, "email-invalido", 15);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
// Saída:
// Erros de validação:
// - O nome não pode ser nulo
// - O email deve ser válido
// - A idade deve ser maior ou igual a 18
Persistindo com JPA:
EntityManager em = ...; // Obtém o EntityManager
em.getTransaction().begin();
em.persist(usuario); // A validação ocorre automaticamente antes de persistir
em.getTransaction().commit();
Conclusão
Para garantir que um objeto seja criado em um estado consistente:
- Use
Assert
para validações simples e diretas. - Use o padrão Builder para objetos complexos.
- Combine Jakarta Validation com
Assert
para validações avançadas. - Torne a classe imutável para evitar alterações inconsistentes após a criação.
Essas práticas garantem que as invariantes sejam respeitadas, mantendo a consistência e a integridade dos dados em seu sistema.
Ao trabalhar com uma entidade JPA:
- Use Jakarta Validation para definir regras de validação declarativas.
- Use
Assert
para validações programáticas em métodos setters. - Integre a validação ao ciclo de vida da entidade com
@PrePersist
e@PreUpdate
. - Proteja campos críticos (como o ID) para evitar alterações inconsistentes.
Essas práticas garantem que a entidade esteja sempre em um estado consistente, tanto no domínio da aplicação quanto no banco de dados.
Top comments (0)