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
eweb
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 pacotesjobs
emessaging
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 ouRetorno
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:
-
da API do
Cloudsupport
de consultas básicas disponibilizada por herança deBaseService
,BaseFragment
ouBaseComponent
. -
da API do
Cloudsupport
de operações básicas de alteração da dados disponibilizada por Active Record nas entidades persistentes. -
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” comEndereco
(pessoa.endereco
). Será necessário primeiro persistir oEndereco
(para que este receba o ID) e depois o objetoPessoa
. Sem o flush, o JPA permitiria a ordem inversa dessas operações (persistirPessoa
antes deEndereco
), mas, neste caso, o JPA primeiro faria o insert dePessoa
com o campo “endereco_id” nulo e depois executaria uma instrução SQL extra para vincular a chave estrangeira chave entrePessoa
eEndereco
(update pessoa set endereco_id = :id
). Observe que há um custo computacional extra nesse caso.
- Suponha uma entidade
-
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
comendereco
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.
- Quando existe mapeamento de campo auto gerado na entidade (que é o mais comum, devido
ao
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:
-
Operações de alta performance que exigem hints específicos do SGBD.
-
Relatórios e consultas desnormalizadas.
-
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
eLoggerFactory
são do pacoteorg.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 tipoLong
. 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. OCloudsupport
disponibiliza a classe utilitáriaUidGenerator
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.