Camada de Negócio

Introdução

A camada de negócio é responsável por implementar as regras de negócio da aplicação.

Ela é composta por features. Cada feature tem seu próprio pacote Java e implementa 1 (uma) funcionalidade negocial.

A funcionalidade negocial deverá ser tão próxima possível de uma Função Transacional (FT) nos termos da APF (Análise de Pontos de Função), ou seja, cada feature seria um processo elementar que constitui uma transação completa e significativa no ponto de vista do usuário. Dessa forma, a organização do código-fonte fica muito próxima do negócio, o que aumenta a legibilidade e manutenibilidade da aplicação.

O presente padrão será doravante chamado de Feature Service.

As seções seguintes detalham o padrão Feature Service, que inclui a caracterização dos 3 (três) estereótipos de classe definidos para organizar o código-fonte da camada de negócio: “Serviço”, “Fragmento de Serviço” e “Componente de Serviço”.

Vantagens do Padrão

O modelo de desenvolvimento do Cloudsupport baseado em features tem as seguintes vantagens:

  • Código-fonte orientado ao requisito, oferecendo maior legibilidade e rápida identificação do escopo funcional da aplicação.

  • Encapsulamento das funcionalidades, o que permite crescimento mais estável do código-fonte, pois a inclusão de novas funcionalidades, em geral, não impactam nas classes existentes.

  • Ausência de classes centralizadas, aquelas que crescem indefinidamente com o aumento da quantidade de funcionalidades e implicam difícil manutenção, como ocorre em anti-padrões como Entity Servive.

  • Possibilidade de geração de um catálogo de funcionalidades, que seria o escopo funcional da aplicação, a partir de inspeção automatizada do código-fonte.

  • Possibilidade de cálculo automatizado de uma estimativa de tamanho funcional em Pontos de Função.

  • Permite análise eficaz de indicadores de referência cíclica e de referência transitiva entre pacotes.

  • Leitura linear do código, sem referência cruzada entre classes e entre pacotes, em aderência ao pattern Acyclic Dependencies Principle.

  • Maior clareza na automação de teste e análise de cobertura, vez que os scripts de testes estarão associados a classes de funcionalidades.

  • Melhor adequação a equipes em que vários desenvolvedores atuam concorrentemente na manutação do código.

Desvantagens:

  • Maior quantidade de classes, o que aumenta a probabilidade de colisão de nomes.

Convenções de Nomes

Cada feature deve ser nomeada conforme as seguintes diretrizes:

  • Adota-se o formato CamelCase.

  • O nome inicia em minúsculo quanto utilizado em pacote Java e em maiúsculo nas classes.

  • O nome é composto tipicamente de “Ação” (verbo no infinitivo) + “Complemento”.

  • Siglas são tratadas como palavras no CamelCase. Exemplos: “Rg”, “Uf”, “CpfDestino” e não “RG”, “UF” e “CPFDestino”.

  • Abrevie somente palavras grandes (tipicamente acima de ~10 letras).

  • Considera-se verbos comuns: Cadastrar, Alterar, Remover, Pesquisar, Detalhar, Gerar, Desfazer.

Exemplos de nomes de features:

GerarOrdemServico
CancelarOrdemServico
EncaminharOrdemServico
ExecutarOrdemServico
AtestarOrdemServico
GlosarOrdemServico
PesquisarOrdensServico

As features são organizadas conforme a estrutura de pacotes detalhada a seguir.

Estrutura de Pacotes

A camada de negócio tem a seguinte estrutura de pacotes:

Pacote Descrição
.services.web Funcionalidades na forma de webservices.
.services.jobs Funcionalidades na forma de rotinas ou processos batch.
.services.messaging Funcionalidades na forma de eventos de mensageria.
.services.common Componentes compartilhados; São classes que implementam regras de negócio reusadas por duas ou mais funcionalidades.

Os pacotes common e web contêm subpacotes para melhor organização do código. Os subpacotes permitem separar as funcionalidades e componentes por assunto. Caso existam muitas rotinas ou muitas integrações por mensageria, os pacotes jobs e messaging também podem conter subpacotes organizadores.

Cada funcionalidade, por sua vez, tem seu próprio pacote.

Exemplo de estrutura de pacotes da camada de negócio de um microsserviço de gerenciamento de Ordens de Serviço:

services
   common
   web
      apoio
         pesquisarMotivosCancelamento
         pesquisarSituacoesOs
         pesquisarTiposExecutores
      executor
         cadastrarExecutor
         inativarExecutor
         pesquisarExecutores
         reativarExecutor
      ordemServico
         atestarOrdemServico
         cancelarOrdemServico
         encaminharOrdemServico
         executarOrdemServico
         gerarOrdemServico
         glosarOrdemServico
         pesquisarOrdensServico

Dentro do pacote de cada feature, as classes são implementadas conforme os estereótipos, ou “tipos” de classes, explanados a seguir.

Estereótipos de Classes

O estereótipo, no contexto da presente arquitetura, é uma marcação na classe, via anotação Java, para indicar seu objetivo. Isso é muito importante para que outros desenvolvedores possam entender melhor seu código e realizar manutenções ou correções.

Os estereótipos aqui propostos fazem parte do padrão Feature Service do Cloudsupport e visam tornar o código-fonte mais legível. A responsabilidade da classe Java, no sentido do que ela pode ou não pode fazer, depende de seu estereótipo.

Cada feature é composta por uma ou mais classes, conforme a necessidade de organização do código-fonte. A presente arquitetura define três estereótipos, seguindo o Single Responsibility Principle, para as classes da feature:

  • @Service: Utilizado na classe que implementa a funcionalidade propriamente dita, ou seja, a classe que contém o ponto de entrada da transação. Existe sempre e apenas 1 (uma) classe de serviço por feature. Cada funcionalidade possui seu próprio pacote Java.

  • @Fragment: Quando o código-fonte da classe de serviço se torna extenso, ele pode ser dividido em pedaços, sendo cada um deles uma classe @Fragment. O @Fragment é, portanto, um trecho das regras de negócio, como por exemplo uma etapa do fluxo de cálculo da funcionalidade. A classe de serviço pode ser subdividida em várias classes de fragmento, quantas forem necessárias para trazer clareza e legibilidade para o código-fonte. Os fragmentos ficam localizados dentro do pacote da feature. Podem ser criados subpacotes caso houver muitos fragmentos.

  • @Component: Utilizado nas classes que implementam regras de negócio reusadas em duas ou mais funcionalidades. As classes de componente ficam no pacote .common.<assunto>, e não dentro do pacote da feature.

A seguir são definidas as diretrizes para cada estereótipo.

@Service

  • Herdam de BaseService e são anotadas com @Service.

  • Recebem o nome da funcionalidade mais sufixo Service. Exemplo: “CancelarCobrancaService”.

  • São singletons e stateless, não devendo manter estados em seus atributos.

  • Controlam transação, via @Transactional (do Spring) em seu método público.

    • Evitar throws na assinatura do método transacional pois, por padrão, @Transactional faz rollback somente para exceções unchecked.

    • Declarar @Transactional(readOnly = true) caso o serviço realize apenas consulta no banco de dados.

  • Quando invocadas, a qualquer momento, devem sempre levar o sistema a um estado consistente.

  • Em geral representam uma Função Transacional, no sentido de APF.

  • Contêm, via de regra, apenas 1 (um) método público, referente ao ponto de entrada da funcionalidade. Exemplo: CancelarCobrancaService.cancelar(…).

  • Podem ser divididas em fragmentos (vide classes @Fragment a seguir).

  • Usualmente são classes pequenas (até 200 linhas úteis).

  • Podem fazer LOG.

  • Podem tratar exceção, desde que haja regra de negócio definida para o fluxo de erro.

  • Devem lançar exceção do tipo BusinessException para emitir mensagens de erro de negócio, que serão exibidas para o usuário.

  • Não possuem getters e setters de atributos injetados.

  • Recebem parâmetros externos ao serviço e retornam dados através de DTOs (Data Transfer Objects) e nunca com uso de entidades persistentes. Os DTOs:

    • São colocados no pacote da feature.

    • São prefixados com o nome da funcionalidade.

    • Não precisam do sufixo DTO.

    • Possuem o sufixo Params se for DTO de entrada ou Retorno se for de saída.

    • Podem possuir a anotação de classe @JsonIgnoreProperties(ignoreUnknown = true).

Exemplo de classe de serviço:

@Service
public class CancelarCobrancaService extends BaseService {

    @Transactional
    public void cancelar(CancelarCobrancaParams params) {

        // Valida os parametros
        this.validarParametros(params);

        // Obtém a entidade e valida se a cobranca pode ser cancelada
        Cobranca cobranca = findByUid(Cobranca.class, params.getUid(), "lancamentos");
        this.validarPodeCancelar(cobranca);

        // Faz o cancelamento        
        cobranca.setDataCancelamento(OffsetDateTime.now());
        cobranca.setSituacaoCobranca(SituacaoCobranca.CANCELADA);
        cobranca.merge(); // Active Record do Cloudsupport
    }

    private void validarParametros(CancelarCobrancaParams params)
    {
        if (params.getUid() == null)
            throw new BusinessException("Informe a identificação da cobrança.");
    }

    private void validarPodeCancelar(Cobranca cobranca)
    {
        if (SituacaoCobranca.CANCELADA.equals(cobranca.getSituacaoCobranca()))
            throw new BusinessException("A cobrança já está cancelada.");

        if (SituacaoCobranca.EM_ABERTO.equals(cobranca.getSituacaoCobranca()))
            return;

        throw new BusinessException("A cobrança está "+ cobranca.getSituacaoCobranca() 
          + " e não pode ser cancelada.");
    }
}

Importante: É desencorajada a implementação de classe de serviço na qual métodos privados invocam outros métodos privados. Quando isso ocorre, é orientado que se criem os fragmentos, explanados a seguir.

@Fragment

  • Herdam de BaseFragment e são anotadas com @Fragment.

  • Têm sufixo Fragment.

  • São singletons e stateless, não devendo manter estados em seus atributos.

  • Não controlam ou delimitam transação.

  • Essencialmente servem para subdividir o código procedural de um @Service ou mesmo de outro @Fragment.

  • Devem estar contidas no pacote correspondente à funcionalidade.

  • Tem seu uso restrito ao pacote correspondente à funcionalidade.

  • Não são regras de negócio reusáveis. Caso o fossem, seriam @Component.

  • Contêm normalmente poucos métodos públicos.

  • Usualmente são classes pequenas (até 200 linhas úteis).

  • Podem fazer LOG.

  • Podem tratar exceção, desde que haja regra de negócio definida para o fluxo de erro.

  • Devem lançar exceção do tipo BusinessException para emitir mensagens de erro de negócio, que serão exibidas para o usuário.

  • Não possuem getters e setters de atributos injetados.

  • Podem receber parâmetros e retornar dados na forma de entidades persistentes.

Exemplo de funcionalidade em que o código do serviço foi dividido em fragmentos:

@Service
class GerarCobrancaService extends BaseService {

   @Inject GerarCobrancaValidacaoFragment validacaoFragment;
   @Inject GerarCobrancaCalculoFragment   calculoFragment;

   @Transactional
   public void refaturar(...) {
      //...
      validacaoFragment.validarCobranca(...);
      calculoFragment.calcularLancamentos(...);
      //... 
   }
}

@Service
class GerarCobrancaValidacaoFragment extends BaseFragment {
   public void validarCobranca(...) {...}
   private void validarDadosCliente(...) {...}
   private void validarLancamentosCobranca(...) {...}
}

@Service
class GerarCobrancaCalculoFragment extends BaseFragment {
   public void calcularLancamentos(...) {...}
   private void calcularCobrancas(...) {...}
   private void calcularCreditos(...) {...}
   private void calcularImpostos(...) {...}
}

@Component

  • Herdam de BaseComponent e são anotadas com @Component (do Spring).

  • Têm sufixo Component.

  • São singletons e stateless, não devendo manter estados em seus atributos.

  • Não controlam ou delimitam transação.

  • Em geral representam um CIR (Componente Interno Reusável), no sentido de APF.

  • São necessariamente reusados por mais de uma feature.

  • Não são funcionalidades completas. Caso o fossem, seriam @Service.

  • Ficam no pacote .services.common.<assunto>.

  • Contêm normalmente poucos métodos públicos.

  • Usualmente são classes pequenas (até 200 linhas úteis).

  • Podem fazer LOG.

  • Podem tratar exceção, desde que haja regra de negócio definida para o fluxo de erro.

  • Devem lançar exceção do tipo BusinessException para emitir mensagens de erro de negócio, que serão exibidas para o usuário.

  • Não possuem getters e setters de atributos injetados.

  • Podem receber parâmetros e retornar dados na forma de entidades persistentes.

Operações de Persistência

O acesso à persistência é realizado a partir das classes de negócio, sejam serviços, fragmentos ou componentes, com a utilização:

  1. da API do Cloudsupport de consultas básicas disponibilizada por herança de BaseService, BaseFragment ou BaseComponent.

  2. da API do Cloudsupport de operações básicas de alteração da dados disponibilizada por Active Record nas entidades persistentes.

  3. da API EntityManager do JPA para operações mais elaboradas, que inclui SQL nativo, quando for necessário.

Operações de Consulta

O Cloudsupport oferece alguns métodos para que as classes de negócio possam realizar consultas básicas de dados. Estão disponíveis por herança nas classes de serviço, fragmentos e componentes. Nos exemplos a seguir, considere Entidade uma classe de entidade persistente mapeada com JPA e en uma instância da entidade.

Método Descrição
super.findById(Entidade.class, id, fetch, lockMode) Retorna uma instância da entidade a partir do “id”.
super.findByUid(Entidade.class, uid, fetch, lockMode) Retorna uma instância da entidade a partir do “uid”.
super.findByAttr(Entidade.class, attr, value, fetch, lockMode) Retorna uma instância da entidade a partir da atributo de nome “attr” cujo valor é unicamente identificado pelo parâmetro “value”.
super.findAll(Entidade.class, fetch, orderBy, orderAsc) Retorna todas as instâncias da entidade. Parâmetro orderBy é o nome do campo a ser ordenado e orderAsc vale true para ordenação ascendente.
super.cb() Atalho que retorna o CriteriaBuilder do EntityManager.
super.setFetchJoin(from, fetch) Método útil que aplica FETCH JOIN em um “from” do Criteria JPA, em que fetch é uma String contendo os caminhos das associativas, separados por vírgula.

Em todos os casos acima, o parâmetro fetch é uma String contendo os caminhos das associativas, separados por vírgula, que terão FETCH JOIN.

Operações de Alteração de Dados

A arquitetura provê operações básicas de alteração de dados por Active Record. Nos exemplos a seguir, considere Entidade uma classe de entidade persistente mapeada com JPA e en uma instância da entidade.

Operação Descrição
en.persist() Cadastra a entidade no banco de dados. Equivale a “entityManager.persist(en); entityManager.flush();”. A entidade migra do estado transient para persistent. Utilize “en.persist(false)” para não forçar o flush imediato. Nota: Fazer persist() em instâncias detached implicará exceção.
en = en.merge() Atualiza a entidade no banco de dados. Equivale a “en = entityManager.merge(en); entityManager.flush();”. Se a entidade for detached, a instância persistente é obtida da sessão JPA (ou a partir do banco de dados) e atualizada. Se a entidade for transient, a entidade é criada no banco de dados. A instância persistente é obtida no retorno do método. Utilize “en.merge(false)” para não forçar o flush imediato.
en.remove() Remove a entidade do banco de dados. Equivale a “entityManager.remove(en); entityManager.flush();”. Se a entidade for detached ou transient, ocorrerá exceção. Utilize “en.remove(false)” para não forçar o flush imediato.

Comentário: As operações do Active Record do Cloudsupport fazem flush por padrão. São motivações para esse comportamento padrão:

  • Código mais legível, pois incentiva o desenvolvedor a seguir a ordem natural das operações de banco de dados. Exemplo:

    • Suponha uma entidade Pessoa que tenha uma associação de “muitos para um” com Endereco (pessoa.endereco). Será necessário primeiro persistir o Endereco (para que este receba o ID) e depois o objeto Pessoa. Sem o flush, o JPA permitiria a ordem inversa dessas operações (persistir Pessoa antes de Endereco), mas, neste caso, o JPA primeiro faria o insert de Pessoa com o campo “endereco_id” nulo e depois executaria uma instrução SQL extra para vincular a chave estrangeira chave entre Pessoa e Endereco (update pessoa set endereco_id = :id). Observe que há um custo computacional extra nesse caso.
  • Garantia de que qualquer instrução SQL nativa será executada com o banco de dados atualizado, o que mitiga erros de lógica (como selecionar pessoas com endereco nulo).

  • Pouco ou nenhum impacto de performance.

    • Quando existe mapeamento de campo auto gerado na entidade (que é o mais comum, devido ao @GeneratedValue no ID), a inserção/atualização em massa do Hibernate é desativada. Portanto, não forçar o flush provavelmente não proporcionaria ganho de desempenho. Para operações em massa (bulk update/delete), prefira usar CriteriaUpdate, CriteriaDelete ou SQL nativo.

Operações via EntityManager

As classes de negócio têm acesso completo ao EntityManager para operações de persistência, podendo ser utilizados quaisquer recursos do JPA, conforme necessidade. No que se refere a consultas, o JPQL (Java Persistence Query Language) traz simplicidade e produtividade, podendo ser utilizado livremente.

Método Descrição
super.em() Retorna o EntityManager.

SQL Nativo

SQL nativo é recomendado nos seguintes cenários:

  1. Operações de alta performance que exigem hints específicos do SGBD.

  2. Relatórios e consultas desnormalizadas.

  3. Statements específicos do SGBD não existentes na API do JPA.

Utilize SQL nativo via EntityManager.

Tipos de Funcionalidades

A presente arquitetura define padrões para dois tipos relevantes de features, apresentados abaixo. Outros tipos de funcionalidades podem ser implementados conforme a necessidade do projeto.

Webservices

São funcionalidades projetadas para consumo pelos frontends, sejam aplicativos móveis ou páginas web, bem como para integrações com outros microsserviços. As aplicações de backend são tipicamente compostas de webservices.

Dedicamos seção específica para esse tema: Webservices.

Rotinas (Jobs)

São funcionalidades projetadas para execução em background. São úteis para processos em lote (batch) ou processos de fluxo contínuo (fila ininterrupta, como, por exemplo, envio de notificações).

Dedicamos seção específica para esse tema: Rotinas.

Logging

As classes de negócio possuem o atributo logger, por herança, relativo ao framework de logging. Esse atributo é declarado da seguinte maneira nas classes mãe:

protected final Logger logger = LoggerFactory.getLogger(getClass());

As classes Logger e LoggerFactory são do pacote org.slf4j. O framework que implementa a API do SLF4J é o Log4j2.

As configurações de logging para o ambiente de desenvolvimento local encontram-se no arquivo logging.xml. Os projetos arquétipos incluem uma configuração padrão. Nos ambientes de teste/homologação e produção, as configurações de logging são injetadas na aplicação pelo Kubernetes.

UID

O uid deve ser utilizado como chave das entidades que atravessam a fronteira da aplicação. Não exponha o id sequencial de banco.

Diretrizes:

  • O uid deve ser declarado na entidade preferencialmente com o tipo Long. Strings podem ser utilizadas, porém reduzem a performance do banco.

  • O uid deve ser preenchido pelo microsserviço quando a entidade é cadastrada no banco de dados. O Cloudsupport disponibiliza a classe utilitária UidGenerator para essa finalidade:

    //...
    entidade.setUid(UidGenerator.get().next());
    entidade.persist(); 
    
  • No caso de utilização de outro gerador de UID tipo Long, atente-se:

    • À compatibilidade com o tipo numérico de JSON. No JavaScript os números têm 53 bits de precisão, enquanto o Java são 64 bits. Dessa forma, o gerador de UIDs deve trabalhar dentro da zona numérica segura, para evitar truncamento durante a serialização do DTO para JSON e consequentemente causar um bug silencioso, de difícil identificação.

    • À performance.

    • Ao risco de colisão, especialmente quando houver alta concorrência.

Anti-Padrões

Ressalta-se que os padrões DAO, Repository, Rich Domain e Entity Service não devem ser utilizados.

  • O padrão Rich Domain orienta que as operações de negócio sejam implementadas em classes de entidade. Essa abordagem é totalmente desencorajada, pois tende a resultar em classes muito grandes e com alta taxa de referência cíclica. Ela também fere ao princípio da única responsabilidade (SRP, Single-Responsibility Principle), pois uma mesma classe conterá a implementação de muitas funcionalidades do negócio. Note ainda que isso aumenta o risco de conflitos na reintegração de código.

  • O padrão Entity Service orienta que as operações de negócio sejam implementadas em classes que representam entidades fortes. Essa abordagem é desencorajada, pois incorre nos mesmos problemas do Rich Domain.

  • O padrão REST orienta que as operações de negócio sejam organizadas na forma de CRUDs sobre entidades fortes (ou resources). Nesse requisito, o padrão REST não é recomendado, vez que produz uma API menos legível e que requer um esforço maior de documentação bem como de padronização para garantir uma consistência lógica conforme o crescimento funcional da aplicação. A presente arquitetura recomenda que a API seja orientada ao requisito, com a utilização de verbos, para que a API resultante seja clara e intuitiva, algo como é feito em APIs RPC. Entretanto, são bem-vindas do REST as orientações de utilização do formato JSON bem como de códigos de retorno HTTP. Sugere-se evitar SOAP e XML.

  • O padrão DAO (Data Access Object) define uma camada que abstrai as operações de persistência. O EntityManager do JPA atua, dentre outras funções, como DAO, de forma que não se faz necessário implementar manualmente essa camada.

  • O padrão Repository é semelhante ao DAO, porém atua em um nível mais alto de abstração, como na agregação e organização de objetos de domínio em memória. A implementação de Repository oferecida pelo framework Spring Data não permite encapsulamento das operações dentro de features. Nele, a classe de repositório é única por entidade e cresce indefinidamente com a evolução da aplicação, o que implica sérios problemas de manutenibilidade. Por esse motivo o Repository do Spring Data não deve ser utilizado.

Próximos Passos

A próxima leitura sugerida é a seção Webservices, que detalha os endpoints de webservice da camada de negócio.