d5-framework-dominio
Possui interfaces e classes para:
Objeto de valor
IObjetoValor
Realiza o conceito de objeto de valor. IObjetoValor redeclara equals e hashCode para documentar o contrato de que as implementações devem redefini-los, de modo que duas instâncias com os mesmos atributos sejam iguais. O compilador não impõe esse contrato, pois toda classe já herda equals e hashCode de Object; por isso, prefira herdar de ObjetoValorStringAbstrato, que já os implementa, sempre que o objeto de valor for representado por uma única String.
package dev.sauloaraujo.d5.framework.dominio;
import org.jspecify.annotations.Nullable;
public interface IObjetoValor {
@Override
boolean equals(@Nullable Object objeto);
@Override
int hashCode();
}
O parâmetro de equals é anotado com @Nullable porque sobrescreve Object.equals, cujo contrato admite null.
No tutorial, Endereco, formado por vários atributos, implementa IObjetoValor diretamente.
ObjetoValorStringAbstrato
Classe base para objetos de valor representados por uma única String — um código, uma sigla, um nome. Guarda o valor em um atributo final, valida-o contra null no construtor e implementa equals, hashCode e toString a partir dele.
package dev.sauloaraujo.d5.framework.dominio.impl;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import dev.sauloaraujo.d5.framework.dominio.IObjetoValor;
public abstract class ObjetoValorStringAbstrato implements IObjetoValor {
private final String valor;
protected ObjetoValorStringAbstrato(String valor) {
this.valor = Objects.requireNonNull(valor);
}
@Override
public boolean equals(@Nullable Object objeto) {
if (objeto == null || getClass() != objeto.getClass()) {
return false;
}
var outro = (ObjetoValorStringAbstrato) objeto;
return Objects.equals(valor, outro.valor);
}
@Override
public int hashCode() {
return valor.hashCode();
}
@Override
public String toString() {
return valor;
}
}
Duas instâncias são iguais quando têm a mesma classe concreta (getClass()) e o mesmo valor. Portanto, objetos de valor de tipos diferentes com o mesmo valor nunca são iguais. toString devolve o valor sem alteração — usado pelo backend para serializar e converter essas instâncias como uma String pura; veja Serialização de objetos de valor.
No tutorial, ClienteIdentificador, cujo valor é gerado pelo banco de dados e não tem formato definido pelo domínio, herda dessa classe.
ObjetoValorRegexAbstrato
Estende ObjetoValorStringAbstrato para valores cujo formato pode ser descrito por uma expressão regular. Além do valor, o construtor recebe o formato (a expressão regular) e a mensagem de erro — formatada com String.format, de modo que um %s opcional é substituído pelo valor inválido e um % literal deve ser escrito %% —, e valida com Validate.matchesPattern, que exige que o valor inteiro corresponda à expressão regular — não basta conter um trecho que corresponda. A validação de null, herdada de ObjetoValorStringAbstrato, ocorre antes da validação do formato.
package dev.sauloaraujo.d5.framework.dominio.impl;
import org.apache.commons.lang3.Validate;
public abstract class ObjetoValorRegexAbstrato extends ObjetoValorStringAbstrato {
protected ObjetoValorRegexAbstrato(String valor, String formato, String mensagem) {
super(valor);
Validate.matchesPattern(valor, formato, mensagem, valor);
}
}
No tutorial, UnidadeFederativaSigla e UnidadeFederativaCodigoIbge herdam dessa classe.
Entidade
IIdentificador
Identificador de uma entidade. É um IObjetoValor — portanto, comparado por valor — que adicionalmente redeclara toString para documentar que ele deve devolver a representação textual do identificador, usada, por exemplo, em URLs e mensagens de erro. Assim como em IObjetoValor, o compilador não impõe esse contrato; ObjetoValorStringAbstrato já o cumpre, devolvendo o próprio valor.
package dev.sauloaraujo.d5.framework.dominio;
public interface IIdentificador extends IObjetoValor {
@Override
String toString();
}
No tutorial, UnidadeFederativaCodigoIbge implementa essa interface.
IEntidade
Realiza o conceito de entidade. IEntidade é parametrizada pelo tipo I do identificador (um IIdentificador) e expõe getIdentificador, que devolve um Optional vazio enquanto a entidade ainda não tem identificador atribuído.
package dev.sauloaraujo.d5.framework.dominio;
import java.util.Optional;
public interface IEntidade<I extends IIdentificador> {
Optional<I> getIdentificador();
}
Agregado
IAgregado
Realiza o conceito de agregado: sua raiz. IAgregado estende IEntidade<I> e não acrescenta nada além disso — é um marcador da raiz do agregado, parametrizado pelo tipo do identificador.
package dev.sauloaraujo.d5.framework.dominio;
public interface IAgregado<I extends IIdentificador> extends IEntidade<I> {
}
No tutorial, UnidadeFederativa implementa IAgregado<UnidadeFederativaCodigoIbge>, com um identificador sempre presente. Já Cliente implementa IAgregado<ClienteIdentificador> com um identificador gerado pelo banco de dados, que só está presente depois da inclusão.
Repositório
IAgregadoRepositorio
Realiza o conceito de repositório. IAgregadoRepositorio é parametrizada pelo identificador I e pelo agregado A, e expõe incluir, existePorIdentificador, buscarPorIdentificador, alterar e excluir.
package dev.sauloaraujo.d5.framework.dominio.spi;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.IAgregado;
import dev.sauloaraujo.d5.framework.dominio.IIdentificador;
public interface IAgregadoRepositorio<I extends IIdentificador, A extends IAgregado<I>> {
A incluir(A agregado);
boolean existePorIdentificador(I identificador);
A buscarPorIdentificador(I identificador) throws AgregadoInexistenteExcecao;
void alterar(A agregado);
void excluir(I identificador);
}
Implementações são registradas automaticamente como beans — veja Registro de beans.
Autorização
IAgregadoAutorizacoes
Representa se o usuário está autorizado a executar, sobre um agregado, cada uma das quatro operações de um serviço: getIncluir, getBuscar, getAlterar e getExcluir. É parametrizada pelo agregado A.
package dev.sauloaraujo.d5.framework.dominio;
public interface IAgregadoAutorizacoes<A extends IAgregado<?>> {
boolean getIncluir();
boolean getBuscar();
boolean getAlterar();
boolean getExcluir();
}
AgregadoAutorizacoes
Implementação de IAgregadoAutorizacoes que recebe os quatro valores booleanos diretamente pelo construtor.
package dev.sauloaraujo.d5.framework.dominio.impl;
import dev.sauloaraujo.d5.framework.dominio.IAgregado;
import dev.sauloaraujo.d5.framework.dominio.IAgregadoAutorizacoes;
public class AgregadoAutorizacoes<A extends IAgregado<?>> implements IAgregadoAutorizacoes<A> {
private final boolean incluir;
private final boolean buscar;
private final boolean alterar;
private final boolean excluir;
public AgregadoAutorizacoes(boolean incluir, boolean buscar, boolean alterar, boolean excluir) {
this.incluir = incluir;
this.buscar = buscar;
this.alterar = alterar;
this.excluir = excluir;
}
@Override
public boolean getIncluir() {
return incluir;
}
@Override
public boolean getBuscar() {
return buscar;
}
@Override
public boolean getAlterar() {
return alterar;
}
@Override
public boolean getExcluir() {
return excluir;
}
}
No tutorial, UnidadeFederativaServico devolve um AgregadoAutorizacoes<UnidadeFederativa> que libera a busca a qualquer usuário e as demais operações somente a administradores. Já MunicipioAutorizacoes estende AgregadoAutorizacoes, acrescentando a autorização da ação de extinguir.
Segurança
ISegurancaServico
Dá acesso a informações do usuário autenticado: getUsuario, getAutorizacoes e o método default possuiAutorizacao, que verifica se uma autorização está entre as do usuário. Os três métodos lançam NaoAutenticadoExcecao quando não há usuário autenticado.
package dev.sauloaraujo.d5.framework.dominio.spi;
import java.util.List;
public interface ISegurancaServico {
String getUsuario() throws NaoAutenticadoExcecao;
List<String> getAutorizacoes() throws NaoAutenticadoExcecao;
default boolean possuiAutorizacao(String autorizacao) throws NaoAutenticadoExcecao {
return getAutorizacoes().contains(autorizacao);
}
}
No tutorial, UnidadeFederativaServico recebe um ISegurancaServico no construtor e o consulta em criarAutorizacoes.
Serviço
IAgregadoServico
Realiza o conceito de serviço. IAgregadoServico é parametrizada pelo identificador I, pelo agregado A e pelo tipo U de suas autorizações, e expõe incluir, buscar, alterar, excluir e criarAutorizacoes.
package dev.sauloaraujo.d5.framework.dominio;
import java.util.Optional;
public interface IAgregadoServico<I extends IIdentificador, A extends IAgregado<I>, U extends IAgregadoAutorizacoes<A>> {
A incluir(A agregado);
A buscar(I identificador);
void alterar(A agregado);
void excluir(I identificador);
U criarAutorizacoes(Optional<I> identificador);
}
Implementações são registradas automaticamente como beans — veja Registro de beans.
AgregadoServicoAbstrato
Implementação base de IAgregadoServico. Recebe no construtor o ISegurancaServico, exposto às subclasses por getSegurancaServico, o IAgregadoRepositorio do agregado e os modelos das mensagens de erro para identificador existente e inexistente — modelos no formato de String.format, com um %s substituído pelo identificador. Antes de cada operação, o método protegido validarAutorizacao — que recebe o identificador opcional do agregado e um predicado que seleciona, entre as autorizações, a da operação — chama o método abstrato criarAutorizacoes, que a subclasse implementa, e lança NaoAutorizadoExcecao quando o predicado não libera a operação. incluir passa um identificador vazio; buscar, alterar e excluir, o identificador do agregado.
incluir e alterar chamam validarInclusaoAlteracao, ponto de extensão comum às duas operações que a subclasse pode sobrescrever para impor regras adicionais, como unicidade; validarInclusao e validarAlteracao são os pontos de extensão específicos de cada uma. A ordem das chamadas segue o tipo de cada validação, para que a resposta aponte o motivo real da rejeição: primeiro as validações de argumento, que rejeitam o agregado recebido independentemente do que está gravado; depois as validações de estado do próprio agregado, como sua existência; por fim, as validações de estado em relação aos demais agregados, como a unicidade. Por isso, depois de validarAutorizacao, incluir chama validarInclusao, a verificação de inexistência do identificador e validarInclusaoAlteracao, nessa ordem, e alterar chama a verificação de existência, validarAlteracao e validarInclusaoAlteracao. Na alteração, a existência é verificada antes de validarAlteracao porque um agregado inexistente torna a requisição sem objeto, respondendo 404 antes de qualquer validação do agregado recebido. Assim, uma validação de argumento que depende da operação — como a que impede a inclusão de um agregado já com um identificador gerado pelo banco de dados — fica em validarInclusao ou validarAlteracao, assim como, na alteração, uma validação do estado do agregado existente; validarInclusaoAlteracao fica com as validações em relação aos demais agregados. incluir, ao receber um agregado com identificador, e alterar chamam validarInexistencia e validarExistencia, respectivamente, que lançam AgregadoExistenteExcecao e AgregadoInexistenteExcecao a partir de existePorIdentificador do repositório; cada uma tem uma sobrecarga protegida — recebendo um predicado, um argumento e o modelo da mensagem de erro, formatado com o argumento — que a subclasse pode reutilizar para impor outras regras de unicidade ou existência. alterar exige que o agregado tenha identificador; sem ele, lança NoSuchElementException. excluir chama a verificação de existência e, em seguida, validarExclusao, ponto de extensão análogo para a exclusão.
package dev.sauloaraujo.d5.framework.dominio.impl;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import dev.sauloaraujo.d5.framework.dominio.AgregadoExistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.IAgregado;
import dev.sauloaraujo.d5.framework.dominio.IAgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.dominio.IAgregadoServico;
import dev.sauloaraujo.d5.framework.dominio.IIdentificador;
import dev.sauloaraujo.d5.framework.dominio.spi.IAgregadoRepositorio;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
import dev.sauloaraujo.d5.framework.dominio.spi.NaoAutorizadoExcecao;
public abstract class AgregadoServicoAbstrato<I extends IIdentificador, A extends IAgregado<I>, U extends IAgregadoAutorizacoes<A>>
implements IAgregadoServico<I, A, U> {
private final ISegurancaServico segurancaServico;
private final IAgregadoRepositorio<I, A> repositorio;
private final String existenteModelo;
private final String inexistenteModelo;
protected AgregadoServicoAbstrato(ISegurancaServico segurancaServico, IAgregadoRepositorio<I, A> repositorio,
String existenteModelo, String inexistenteModelo) {
this.segurancaServico = Objects.requireNonNull(segurancaServico);
this.repositorio = Objects.requireNonNull(repositorio);
this.existenteModelo = Objects.requireNonNull(existenteModelo);
this.inexistenteModelo = Objects.requireNonNull(inexistenteModelo);
}
protected ISegurancaServico getSegurancaServico() {
return segurancaServico;
}
@Override
public A incluir(A agregado) {
Objects.requireNonNull(agregado);
validarAutorizacao(Optional.empty(), IAgregadoAutorizacoes::getIncluir);
validarInclusao(agregado);
var identificador = agregado.getIdentificador();
if (identificador.isPresent()) {
validarInexistencia(identificador.get());
}
validarInclusaoAlteracao(agregado);
return repositorio.incluir(agregado);
}
protected void validarAutorizacao(Optional<I> identificador, Predicate<U> autorizacao) {
if (!autorizacao.test(criarAutorizacoes(identificador))) {
throw new NaoAutorizadoExcecao("Ação não autorizada");
}
}
protected void validarInexistencia(I identificador) {
validarInexistencia(repositorio::existePorIdentificador, identificador, existenteModelo);
}
protected <T> void validarInexistencia(Predicate<T> predicado, T argumento, String modeloExistente) {
Objects.requireNonNull(predicado);
Objects.requireNonNull(modeloExistente);
if (predicado.test(argumento)) {
var mensagem = String.format(modeloExistente, argumento);
throw new AgregadoExistenteExcecao(mensagem);
}
}
protected void validarInclusaoAlteracao(A agregado) {
}
protected void validarInclusao(A agregado) {
}
@Override
public A buscar(I identificador) {
Objects.requireNonNull(identificador);
validarAutorizacao(Optional.of(identificador), IAgregadoAutorizacoes::getBuscar);
return repositorio.buscarPorIdentificador(identificador);
}
@Override
public void alterar(A agregado) {
Objects.requireNonNull(agregado);
var identificador = agregado.getIdentificador().orElseThrow();
validarAutorizacao(Optional.of(identificador), IAgregadoAutorizacoes::getAlterar);
validarExistencia(identificador);
validarAlteracao(agregado);
validarInclusaoAlteracao(agregado);
repositorio.alterar(agregado);
}
protected void validarExistencia(I identificador) {
validarExistencia(repositorio::existePorIdentificador, identificador, inexistenteModelo);
}
protected <T> void validarExistencia(Predicate<T> predicado, T argumento, String modeloInexistente) {
Objects.requireNonNull(predicado);
Objects.requireNonNull(modeloInexistente);
if (!predicado.test(argumento)) {
var mensagem = String.format(modeloInexistente, argumento);
throw new AgregadoInexistenteExcecao(mensagem);
}
}
protected void validarAlteracao(A agregado) {
}
@Override
public void excluir(I identificador) {
Objects.requireNonNull(identificador);
validarAutorizacao(Optional.of(identificador), IAgregadoAutorizacoes::getExcluir);
validarExistencia(identificador);
validarExclusao(identificador);
repositorio.excluir(identificador);
}
protected void validarExclusao(I identificador) {
}
}
No tutorial, UnidadeFederativaServico estende AgregadoServicoAbstrato e sobrescreve criarAutorizacoes e validarInclusaoAlteracao; na lição 2, passa a sobrescrever também validarExclusao. MunicipioServico sobrescreve ainda validarInclusao e validarAlteracao e acrescenta a ação extinguir, que reutiliza validarAutorizacao e validarExistencia; na lição 3, passa a sobrescrever também validarExclusao. Já ClienteServico sobrescreve validarInclusao, para rejeitar um cliente que chegue à inclusão com o identificador gerado pelo banco de dados, e validarInclusaoAlteracao.
Exceções
AgregadoExistenteExcecao
Estende IllegalStateException. Lançada por AgregadoServicoAbstrato.validarInexistencia quando o identificador do agregado a incluir já existe; a subclasse pode reutilizar esse mesmo método para impor outras regras de unicidade. O backend converte essa exceção em uma resposta 409 Conflict.
package dev.sauloaraujo.d5.framework.dominio;
import java.util.Objects;
public class AgregadoExistenteExcecao extends IllegalStateException {
private static final long serialVersionUID = 5561053670486841675L;
public AgregadoExistenteExcecao(String mensagem) {
super(Objects.requireNonNull(mensagem));
}
}
No tutorial, UnidadeFederativaServico a lança — através de validarInexistencia — quando a sigla ou o nome já pertencem a outra unidade federativa.
AgregadoInexistenteExcecao
Estende NoSuchElementException. Declarada por IAgregadoRepositorio.buscarPorIdentificador, e lançada por AgregadoServicoAbstrato.validarExistencia ao alterar ou excluir um agregado cujo identificador não existe. O backend converte essa exceção em uma resposta 404 Not Found.
package dev.sauloaraujo.d5.framework.dominio;
import java.util.NoSuchElementException;
import java.util.Objects;
public class AgregadoInexistenteExcecao extends NoSuchElementException {
private static final long serialVersionUID = -6427625553855957282L;
public AgregadoInexistenteExcecao(String mensagem) {
super(Objects.requireNonNull(mensagem));
}
}
NaoAutorizadoExcecao
Estende RuntimeException. Lançada por AgregadoServicoAbstrato.validarAutorizacao e por AgregadoAplicacaoServicoAbstrato quando as autorizações do usuário não liberam a operação. O backend converte essa exceção em uma resposta 403 Forbidden.
package dev.sauloaraujo.d5.framework.dominio.spi;
import java.util.Objects;
public class NaoAutorizadoExcecao extends RuntimeException {
private static final long serialVersionUID = -5340214263571227790L;
public NaoAutorizadoExcecao(String mensagem) {
super(Objects.requireNonNull(mensagem));
}
}
NaoAutenticadoExcecao
Estende RuntimeException. Lançada pelos métodos de ISegurancaServico quando não há usuário autenticado. O backend converte essa exceção em uma resposta 401 Unauthorized.
package dev.sauloaraujo.d5.framework.dominio.spi;
import java.util.Objects;
public class NaoAutenticadoExcecao extends RuntimeException {
private static final long serialVersionUID = 8201692097715103859L;
public NaoAutenticadoExcecao(String mensagem) {
super(Objects.requireNonNull(mensagem));
}
}