Arquitetura empresarial

Separar operações da aplicação, regras do domínio e persistência, reconhecendo padrões e os seus custos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Terminar a partida 7 exige validar o estado, calcular a pontuação final e guardar o resultado. Trocar Swing por uma API não deve mudar essas regras; trocar ficheiros por SQL não deve mudar a operação. MVC organiza a apresentação. As operações da aplicação, o domínio e a persistência precisam também de contratos próprios.

Camadas e dependências

Podemos distinguir apresentação, operações da aplicação, domínio e persistência. Uma camada é um grupo de responsabilidades, não necessariamente uma pasta ou um processo diferente.

A apresentação recebe o pedido. Uma operação da aplicação coordena o caso de uso. O domínio valida a mudança. Um adaptador persiste o resultado. Os detalhes de SQL ou ficheiros não precisam de entrar nas entidades do domínio.

No caso terminar a partida 7, com entidade, serviço e repositório, a entidade protege a regra:

class Partida {
    enum Estado { EM_CURSO, TERMINADA }
    private Estado estado = Estado.EM_CURSO;
    private int pontos;
    Partida(int pontos) { this.pontos = pontos; }
    void terminar() {
        if (estado != Estado.EM_CURSO) {
            throw new IllegalStateException("partida ja terminada");
        }
        if (pontos < 0) pontos = 0;
        estado = Estado.TERMINADA;
    }
    boolean terminada() { return estado == Estado.TERMINADA; }
    int getPontos() { return pontos; }
}

O serviço coordena procura, operação e persistência, distinguindo ausência de falha:

class TerminarPartida {
    private final Partidas partidas;
    TerminarPartida(Partidas partidas) { this.partidas = partidas; }
    String terminar(long id) {
        Partida partida = partidas.procurarPorId(id)
            .orElseThrow(() -> new IllegalArgumentException("partida inexistente"));
        partida.terminar();
        partidas.guardar(partida);
        return "partida " + id + ": " + partida.getPontos() + " pontos";
    }
}

Compara percursos da mesma operação:

ProcuraTerminar?Apresentação
Partida 7 em curso, com 120 pontosSim, termina e pede guardar.partida 7: 120 pontos, se a escrita terminar.
Consulta válida sem partida 7Não, não há entidade.Exceção de ausência.
Falha de acesso ao armazenamentoNão, a procura falhou.Erro de acesso, nunca ausência confirmada.
Partida 7 já terminadaNão, a regra rejeita.Exceção de estado, sem nova escrita.

A ausência lança IllegalArgumentException; a tentativa de terminar de novo lança IllegalStateException sem chamar guardar, e são falhas distintas de uma consulta que não conseguiu aceder ao armazenamento.

Em memória, HashMap<Long, Partida> pode servir de fake; em SQL, o mapper traduz linhas em entidades válidas. Os excertos mostram contratos, não uma implementação completa da persistência, e os erros de acesso precisam de ser traduzidos ou declarados conforme a implementação escolhida.

chamadas em execução: Apresentacao -> TerminarPartida -> Partida, Partidas
dependências de código: TerminarPartida --> Partidas; PartidasMemoria --> Partidas; nada no domínio importa SQL

Uma Identity Map no âmbito da operação devolve a mesma instância nas duas leituras da partida 7, evitando dois objetos divergentes; não torna as escritas atómicas nem impede outra operação de alterar a mesma linha, pois isso pertence à transação.

Uma falha depois da alteração em memória e antes de guardar exige uma política; não basta dividir o código em camadas para tornar a operação atómica, por isso é preciso definir o que fica confirmado e o que pode ser repetido.

Transaction Script e Domain Model

Transaction Script organiza uma operação como um procedimento que recebe dados, aplica regras e escreve o resultado. Para operações simples e independentes, pode ser claro e direto. Se as mesmas regras surgem em muitos procedimentos, a duplicação torna mudanças mais difíceis.

Domain Model coloca estado e comportamento em objetos que representam conceitos do domínio. Uma Partida oferece terminar e protege o seu estado, em vez de qualquer cliente alterar um campo público. É útil quando as regras e relações precisam de evoluir em conjunto.

A diferença não é usar classes ou funções: um Transaction Script também pode ser um método de uma classe. Pergunta onde ficam as regras e quem protege os invariantes.

Para uma pontuação lida e mostrada sem regras adicionais, um procedimento pequeno pode chegar. Para uma partida com estados, penalizações e condições de encerramento, um modelo com operações próprias pode tornar o contrato mais claro.

Service Layer

Uma Service Layer define as operações da aplicação, como terminarPartida(id). Coordena o trabalho e pode definir a fronteira transacional. É um ponto de entrada para diferentes interfaces, como uma janela e uma API.

Não precisa de absorver toda a lógica do domínio. Se um serviço calcula manualmente tudo através de getters e setters, as entidades podem ter perdido a responsabilidade pelos seus invariantes.

O serviço pode delegar a validação em Partida, usar um repositório e devolver um resultado adequado à apresentação. Assim, trocar Swing por outra interface não exige reescrever o caso de uso.

Active Record e Data Mapper

Active Record combina um objeto que representa uma linha com operações de persistência e, por vezes, regras desse registo. Um método resultado.guardar() faz o objeto conhecer a forma de guardar. Pode simplificar operações centradas em dados, mas liga o objeto à persistência.

Data Mapper coloca a tradução entre objetos e armazenamento num componente separado. O domínio não precisa de conhecer tabelas nem SQL. O mapper lê valores, constrói objetos válidos e escreve a representação correspondente.

Escolhe pela complexidade e pela independência de que o domínio precisa. Criar uma classe chamada Mapper que só passa chamadas à entidade não separa a persistência se a entidade continua a emitir SQL.

Repository

Um Repository oferece acesso a objetos do domínio com uma interface semelhante a uma coleção. Pode ter procurarPorId e guardar. Esconde a tradução entre consultas e objetos, normalmente usando mappers por baixo.

Este excerto mostra um contrato que não expõe SQL:

interface Partidas {
    java.util.Optional<Partida> procurarPorId(long id);
    void guardar(Partida partida);
}

O tipo Partida pertence ao domínio. Uma implementação em memória serve para testes e uma implementação SQL trata o armazenamento. Optional.empty() representa ausência; uma falha de ligação não deve fingir que a partida não existe.

Não acrescentes um método genérico que recebe strings SQL ao contrato para contornar a fronteira. Se uma nova operação precisa de encontrar partidas em curso, acrescenta uma consulta com esse significado e decide a sua semântica.

Unit of Work e Identity Map

Uma Unit of Work acompanha objetos novos, alterados e removidos durante uma operação e coordena a escrita dessas mudanças. Ajuda a aplicar um conjunto coerente de alterações. Não substitui as garantias transacionais nem resolve sozinha acessos concorrentes.

Uma Identity Map conserva uma instância por identidade dentro de um âmbito. Se carregares duas vezes a partida 7 na mesma operação, evita obter dois objetos independentes que possam divergir. O âmbito deve ser definido, por exemplo uma operação, não um mapa global que cresce sem limite.

Identidade de uma entidade, como o identificador da partida, difere da igualdade de um valor, como duas posições com as mesmas coordenadas. Esta distinção orienta a forma de atualizar e persistir objetos.

DTO

Um Data Transfer Object, DTO, transporta os dados necessários numa fronteira. Pode reunir identificador e pontuação sem permitir que a interface altere diretamente a entidade. O DTO não tem de conter todas as operações do domínio.

Lazy Load

Lazy Load adia o carregamento até os dados serem necessários. Pode evitar trabalho, mas torna um acesso aparentemente simples numa operação de I/O. Se cada linha de uma lista dispara outra consulta, surge o problema de N+1 consultas. A ausência de uma consulta no construtor não significa ausência de custo.

Seguir um caso de uso

Para terminar a partida 7, a apresentação pede terminar(7) ao serviço, o que mostra onde vive cada regra e que padrão guarda cada parte. O serviço obtém a entidade através de Partidas, trata a ausência com erro explícito, pede partida.terminar(), persiste com guardar e devolve o texto com identificador e pontuação mostrado no excerto. Uma API poderia devolver um DTO com esses campos em vez do texto. Nos dois casos, a apresentação não altera os campos internos da entidade.

Testa separadamente a regra de encerramento e a coordenação. Num teste do serviço, uma implementação em memória pode confirmar qual partida foi alterada. Um teste de integração da persistência verifica que o registo realmente sobrevive a uma nova leitura. Uma verificação de chamadas a um mock não prova essa durabilidade.

Cada padrão responde a uma dificuldade diferente. Explica primeiro a regra e a fronteira que precisas, e só depois escolhe o nome do padrão que descreve a solução.

Exercícios

Quem protege o encerramento?

Terminar uma partida exige que esteja em curso e calcula uma penalização própria do seu estado. Num Domain Model, onde colocarias esta regra e o que deixarias ao serviço?

Pista
Distingue uma regra da partida da coordenação de uma operação.
Mais uma pista

A entidade pode oferecer terminar(), em vez de setters para estado e pontuação.

Solução

Partida.terminar() valida o estado e aplica a penalização preservando invariantes. O serviço obtém a partida, chama a operação e persiste-a. A apresentação pede o caso de uso e mostra o resultado.

Confere a tua resposta:

  • A entidade Partida protege a transição e calcula a regra.
  • O serviço coordena procura, operação e persistência.
  • A apresentação recebe o resultado sem alterar campos internos.

Erros frequentes

Não coloques toda a regra num serviço que altera livremente campos da entidade.

Voltar à explicação
O que prova que foi guardado?

Um teste verifica uma chamada a repositorio.guardar(partida) num mock. Qual afirmação fica demonstrada por esse teste?

A tua resposta

Pista
O que realmente correu foi o serviço e um substituto.
Mais uma pista
A durabilidade precisa de uma fronteira de armazenamento real.
Solução

O teste prova o pedido do serviço ao repositório. Para provar a persistência, um teste de integração deve guardar no adaptador real e voltar a ler num âmbito novo adequado.

  • Os dados sobrevivem ao reinício da aplicação.. O mock não escreveu num armazenamento real.
  • O serviço pediu ao colaborador que guardasse essa partida.. Essa é a interação observada.
  • O SQL tem a tabela e os parâmetros corretos.. O adaptador SQL não foi exercitado.

Erros frequentes

Uma chamada verificada não demonstra o efeito de I/O de uma implementação que não correu.

Voltar à explicação
Ausência não é falha de acesso

procurarPorId devolve Optional.empty quando não encontra a partida. Uma implementação captura qualquer erro de ligação e devolve também empty. Explica que distinção perdeu.

Pista
Não conseguir consultar é diferente de consultar e não encontrar.
Mais uma pista

A apresentação pode dizer incorretamente que a partida não existe.

Solução

A implementação perdeu a distinção entre ausência de dados e impossibilidade de verificar os dados. Propaga um erro de acesso adequado ou usa um resultado explícito que preserve esse caso. Optional.empty continua a significar ausência após uma consulta válida.

Confere a tua resposta:

  • Distingue um registo inexistente de uma falha de armazenamento.
  • Não apresenta indisponibilidade como ausência confirmada.
  • Propõe erro ou resultado explícito segundo o contrato.

Erros frequentes

Capturar todos os erros e devolver um valor normal pode esconder falhas e provocar decisões incorretas.

Voltar à explicação
Duas cópias da mesma entidade

Durante uma operação, duas consultas carregam a partida 7 em objetos A e B independentes. A primeira parte da operação altera A; a segunda consulta B e vê o estado antigo. Explica o que uma Identity Map resolve, qual deve ser o seu âmbito e por que não substitui uma transação ou a coordenação com outras operações.

Pista

O identificador é o mesmo, mas as referências podem apontar para objetos diferentes.

Mais uma pista

Conservar uma instância não impede outra operação de alterar a mesma linha na base de dados.

Solução

A Identity Map mantém uma instância por identificador durante a operação. A segunda consulta da partida 7 devolve A, por isso as duas partes observam as alterações desse objeto. Limita o mapa ao âmbito decidido e liberta-o no fim. Isto não torna várias escritas atómicas nem impede outra operação de atualizar a mesma partida. Essas garantias dependem da transação e da política de concorrência. Uma Unit of Work pode coordenar mudanças acompanhadas, mas também não fornece sozinha todas essas garantias.

Confere a tua resposta:

  • Devolve a mesma instância para a mesma identidade dentro da operação.
  • Define o âmbito e evita um mapa global ilimitado.
  • Distingue consistência em memória de atomicidade e concorrência no armazenamento.

Erros frequentes

Não confundas identidade persistente da entidade com igualdade de valores nem uses a cache como prova de dados atuais noutras operações.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.