Pular para conteúdo

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.

IObjetoValor.java
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.

ObjetoValorStringAbstrato.java
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.

ObjetoValorRegexAbstrato.java
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.

IIdentificador.java
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.

IEntidade.java
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.

IAgregado.java
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.

IAgregadoRepositorio.java
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.

IAgregadoAutorizacoes.java
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.

AgregadoAutorizacoes.java
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.

ISegurancaServico.java
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.

IAgregadoServico.java
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.

AgregadoServicoAbstrato.java
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.

AgregadoExistenteExcecao.java
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.

AgregadoInexistenteExcecao.java
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.

NaoAutorizadoExcecao.java
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.

NaoAutenticadoExcecao.java
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));
    }
}