Skip to content

4. [TD]: Arquiteturas em camadas

Palavras-chave: arquitetura multicamadas, Spring, injeção de dependências.

4.1. Introdução

Vamos rever o que fizemos:

  • Na Parte 1 do exercício ELECTIONS, não foram utilizadas classes. Construímos uma solução tal como a teríamos construído na linguagem C.
  • Na Parte 2 do exercício, foram introduzidas duas classes:
    • [VoterList], que representa os atributos (id, nome, votos, lugares, eliminado) de uma lista de candidatos
    • [ElectionsException], uma classe para exceções não tratadas. Este tipo de exceção é utilizado sempre que ocorre um erro fatal na aplicação eleitoral. É não tratada, o que significa que o programador não é obrigado a tratá-la com um bloco try-catch.

Até agora, o cálculo dos resultados eleitorais tem sido tratado por um método [main] da classe [MainElections]

package istia.st.elections;

import java.io.*;

public class MainElections {

   // some data
  private static final double barre = 0.05;

  // ----------------------------------------------------------------------
   // the main procedure
  public static void main(String[] arguments) throws IOException {

     // prepare keyboard input stream
    BufferedReader clavier = new BufferedReader(new InputStreamReader(System.in));

     // data entry for seat calculations
...
     // calculation of seats obtained by the various lists
....
     // display of results
...
  } // hand
} // class

A solução anterior inclui três fases padrão:

  • aquisição de dados, linhas 17-18
  • cálculo da solução, linhas 19–20
  • exibição e/ou gravação dos resultados, linhas 21–22

Apenas a fase 2 é verdadeiramente constante. A fase 1 pode variar: os dados podem provir do teclado, como nos exemplos estudados, de um ficheiro de texto, de uma interface gráfica, de uma base de dados, da rede, ... Da mesma forma, existem várias formas de apresentar os resultados na fase 3: exibi-los no ecrã, como feito nos exemplos estudados, guardá-los num ficheiro, numa base de dados, enviá-los pela rede, etc.

De forma mais geral, uma aplicação pode frequentemente ser modelada como três camadas, cada uma com uma função bem definida:

Esta arquitetura é também designada por «arquitetura de três camadas». O termo «três camadas» refere-se normalmente a uma arquitetura em que cada camada se encontra numa máquina diferente. Quando as camadas se encontram na mesma máquina, a arquitetura passa a ser uma «arquitetura de três níveis».

  • A camada [de negócios] contém as regras de negócio da aplicação. Para a nossa aplicação eleitoral, estas são as regras que calculam os lugares conquistados pelas diferentes listas, assim que os votos obtidos por cada uma forem conhecidos. Esta camada requer dados para funcionar. Por exemplo, na aplicação eleitoral:
  • as listas, cada uma com o seu nome e número de votos
  • o número de lugares a preencher
  • o limiar eleitoral abaixo do qual uma lista é eliminada

No diagrama acima, os dados podem provir de duas fontes:

  • a camada de acesso aos dados ou [DAO] (DAO = Data Access Object) para dados já armazenados em ficheiros ou bases de dados. Este pode ser o caso aqui para os nomes das listas, o número de lugares a preencher e o limiar eleitoral. De facto, esta informação é conhecida antes da própria eleição.
  • a camada de interface do utilizador ou [ui] (UI = Interface do Utilizador) para dados introduzidos pelo utilizador ou apresentados ao utilizador. Este pode ser o caso aqui para os votos nas listas, que só são conhecidos no último momento, bem como para a apresentação dos resultados eleitorais.
  • De um modo geral, a camada [DAO] lida com o acesso a dados persistentes (ficheiros, bases de dados) ou não persistentes (rede, sensores, etc.).
  • A camada [UI], por outro lado, lida com as interações com o utilizador, caso haja algum.
  • As três camadas são tornadas independentes através da utilização de interfaces Java.
  • Existem vários métodos para integrar estas camadas na aplicação. Iremos utilizar uma ferramenta chamada «Spring». No diagrama, esta atravessa as outras camadas.

Vamos revisitar a aplicação [Elections] desenvolvida anteriormente para lhe conferir uma arquitetura de três camadas. Para tal, examinaremos as camadas [UI, Business, DAO] uma a uma, começando pela camada [DAO], que lida com dados persistentes.

Primeiro, precisamos de definir as interfaces para as diferentes camadas da aplicação [Elections].

4.2. As Interfaces da Aplicação [Elections]

Lembre-se de que uma interface define um conjunto de assinaturas de métodos. As classes que implementam a interface fornecem a implementação para esses métodos.

Voltemos à arquitetura de 3 camadas da nossa aplicação:

Neste tipo de arquitetura, é frequentemente o utilizador que toma a iniciativa. Ele faz um pedido em [1] e recebe uma resposta em [8]. Isto é chamado de ciclo pedido-resposta. Tomemos o exemplo do cálculo dos lugares conquistados na noite das eleições. Isto exigirá várias etapas:

  1. A camada [ui] terá de perguntar ao utilizador o número de votos recebidos por cada uma das listas. Para tal, terá de apresentar ao utilizador os nomes das listas concorrentes. O utilizador terá então de introduzir simplesmente o número de votos ao lado de cada lista e solicitar o cálculo dos lugares.
  2. A camada [ui] não possui os nomes das listas. Estes estão armazenados na fonte de dados à direita do diagrama. Ela utilizará o caminho [2, 3, 4, 5, 6, 7] para os recuperar. A operação [2] é o pedido das listas e a operação [7] é a resposta a esse pedido. Uma vez feito isto, pode apresentá-las ao utilizador através de [8].
  3. O utilizador transmitirá à camada [ui] o número de votos obtidos por cada lista. Esta é a operação [1] acima. Durante esta etapa, o utilizador interage apenas com a camada [ui]. É esta camada que verificará a validade dos dados introduzidos. Uma vez feito isto, o utilizador solicitará a lista de lugares obtidos por cada lista.
  4. A camada [ui] solicitará à camada de negócios que calcule os lugares. Para tal, enviará os dados que recebeu do utilizador para a camada de negócios. Esta é a operação [2].
  5. A camada [business] necessita de determinadas informações para realizar a sua tarefa. Já dispõe das listas da operação (b). Também necessita do número de lugares a preencher e do valor do limiar eleitoral. Solicitará estas informações à camada [DAO] através do caminho [3, 4, 5, 6]. [3] é o pedido inicial e [6] é a resposta a esse pedido.
  6. Com todos os dados de que necessitava, a camada [business] calcula os lugares conquistados por cada lista.
  7. A camada [business] pode agora responder ao pedido da camada [ui] feito em (d). Este é o caminho [7].
  8. A camada [UI] irá formatar estes resultados para os apresentar ao utilizador de forma adequada e, em seguida, exibi-los. Este é o caminho [8].
  9. É possível imaginar que estes resultados precisam de ser armazenados num ficheiro ou numa base de dados. Isto pode ser feito automaticamente. Neste caso, após a operação (f), a camada [business] solicitará à camada [DAO] que guarde os resultados. Este será o caminho [3, 4, 5, 6]. Isto também pode ser feito apenas mediante solicitação do utilizador. O caminho [1-8] será utilizado pelo ciclo de solicitação-resposta.

Podemos ver nesta descrição que uma camada utiliza os recursos da camada à sua direita, nunca os da camada à sua esquerda. Considere duas camadas contíguas:

A camada [A] faz pedidos à camada [B]. Nos casos mais simples, uma camada é implementada por uma única classe. Uma aplicação evolui ao longo do tempo. Assim, a camada [B] pode ter diferentes classes de implementação [B1, B2, ...]. Se a camada [B] for a camada [DAO], pode ter uma implementação inicial [B1] que recupera dados de um ficheiro. Alguns anos mais tarde, poderemos querer armazenar os dados numa base de dados. Criaremos então uma segunda classe de implementação [B2]. Se, na aplicação inicial, a camada [A] trabalhava diretamente com a classe [B1], somos obrigados a reescrever parcialmente o código da camada [A]. Suponhamos, por exemplo, que escrevemos algo como o seguinte na camada [A]:

1
2
3
B1 b1=new B1(...);
..
b1.getData(...);
  • linha 1: é criada uma instância da classe [B1]
  • linha 3: são solicitados dados a esta instância

Se assumirmos que a nova classe de implementação [B2] utiliza métodos com a mesma assinatura que os da classe [B1], teremos de alterar todas as referências a [B1] para [B2]. Este é um cenário muito favorável e bastante improvável, caso não tenhamos prestado atenção a estas assinaturas de métodos. Na prática, é comum que as classes [B1] e [B2] tenham assinaturas de métodos diferentes, o que significa que uma parte significativa da camada [A] terá de ser completamente reescrita.

Podemos melhorar a situação introduzindo uma interface entre as camadas [A] e [B]. Isto significa que definimos as assinaturas dos métodos apresentados pela camada [B] à camada [A] numa interface. O diagrama anterior passa então a ser o seguinte:

A camada [A] já não comunica diretamente com a camada [B], mas sim com a sua interface [IB]. Assim, no código da camada [A], a classe de implementação [Bi] da camada [B] aparece apenas uma vez, no momento da implementação da interface [IB]. Uma vez feito isso, é a interface [IB] — e não a sua classe de implementação — que é utilizada no código. O código anterior passa a ser o seguinte:

1
2
3
IB ib=new B1(...);
..
ib.getData(...);
  • linha 1: é criada uma instância [ib] que implementa a interface [IB] através da instanciação da classe [B1]
  • linha 3: são solicitados dados à instância [ib]

Agora, se substituirmos a implementação [B1] da camada [B] por uma implementação [B2], e ambas as implementações seguirem a mesma interface [IB], então apenas a linha 1 da camada [A] precisa de ser modificada, e nenhuma outra linha. Esta é uma grande vantagem que, por si só, justifica o uso sistemático de interfaces entre duas camadas.

Podemos ir ainda mais longe e tornar a camada [A] completamente independente da camada [B]. No código acima, a linha 1 coloca um problema porque codifica de forma rígida uma referência à classe [B1]. Idealmente, a camada [A] deveria poder utilizar uma implementação da interface [IB] sem ter de especificar um nome de classe. Isto seria consistente com o nosso diagrama acima. Podemos ver que a camada [A] interage com a interface [IB], e não há razão para que ela precise de saber o nome da classe que implementa esta interface. Este detalhe não é útil para a camada [A].

O framework Spring (http://www.springframework.org) permite-nos alcançar este resultado. A arquitetura anterior evolui da seguinte forma:

A camada transversal [Spring] permitirá que uma camada obtenha, através da configuração, uma referência à camada à sua direita sem precisar de saber o nome da classe que implementa essa camada. Este nome estará nos ficheiros de configuração e não no código Java. O código Java para a camada [A] assume então a seguinte forma:

1
2
3
IB ib; // initialisé par Spring
..
ib.getData(...);
  • linha 1: uma instância [ib] que implementa a interface [IB] da camada [B]. Esta instância é criada pelo Spring com base nas informações encontradas num ficheiro de configuração. O Spring encarregar-se-á de criar:
    • a instância [b] que implementa a camada [B]
    • a instância [a] que implementa a camada [A]. Esta instância será inicializada. Ao campo [ib] acima será atribuída a referência [b] do objeto que implementa a camada [B]
  • linha 3: são solicitados dados à instância [ib]

Podemos agora ver que a classe de implementação [B1] da camada B não aparece em nenhum ponto do código da camada [A]. Quando a implementação [B1] for substituída por uma nova implementação [B2], nada mudará no código da classe [A]. Basta alterar os ficheiros de configuração do Spring para instanciar [B2] em vez de [B1].

A combinação do Spring com as interfaces Java traz uma melhoria decisiva à manutenção da aplicação, tornando as camadas da aplicação fortemente acopladas entre si. Esta é a solução que utilizaremos para a aplicação [Elections].

Voltemos à arquitetura de três camadas da nossa aplicação:

Em casos simples, podemos começar pela camada [de negócios] para descobrir as interfaces da aplicação. Para funcionar, ela precisa de dados:

  • já disponíveis em ficheiros, bases de dados ou através da rede. Estes dados são fornecidos pela camada [DAO].
  • ainda não disponíveis. São então fornecidos pela camada [UI], que os obtém do utilizador da aplicação.

Que interface deve a camada [DAO] fornecer à camada [business]? Que interações são possíveis entre estas duas camadas? A camada [DAO] deve fornecer os seguintes dados à camada [business]:

  • o número de lugares a preencher
  • o limiar eleitoral abaixo do qual uma lista é eliminada
  • os nomes das listas

Esta informação é conhecida antes da eleição e pode, portanto, ser armazenada. Na direção [negócio] -> [DAO], a camada [negócio] pode solicitar à camada [DAO] que registe os resultados da eleição, incluindo o número de lugares conquistados pelas várias listas.

Com esta informação, poderíamos tentar uma definição inicial da interface da camada [DAO]:


public interface IElectionsDao {
 
  public double getSeuilElectoral();
 
  public int getNbSiegesAPourvoir();
 
  public ListeElectorale[] getListesElectorales();
 
  public void setListesElectorales(ListeElectorale[] listesElectorales);
}
  • Linha 1: A interface chama-se [IElectionsDao]. Define quatro métodos:
    • três métodos para ler dados da fonte de dados: [getVotingThreshold, getNumberOfSeatsToBeFilled, getVoterLists]. Estes três métodos permitirão à camada [business] obter os dados que caracterizam a eleição atual.
    • um método para gravar dados na fonte de dados: [setVoterLists]. Este método permitirá que a camada [business] solicite o registo dos resultados que calculou.

Voltemos à arquitetura de três camadas da nossa aplicação:

Que interface deve a camada [de negócios] apresentar à camada [ui]? Vamos examinar as possíveis interações entre estas duas camadas.

  1. A camada [ui] será responsável por solicitar votos ao utilizador para as várias listas concorrentes. Para tal, deve conhecer o número de listas. Pode solicitar esta informação à camada [business], que, por sua vez, pode solicitar a tabela de listas concorrentes à camada [dao]. Se a camada [business] tiver esta tabela, mais vale transferi-la para a camada [UI]. A camada [UI] terá então os nomes das listas e poderá refinar as suas mensagens ao utilizador perguntando, por exemplo, «Número de votos para a Lista A».
  2. Assim que a camada [UI] tiver obtido os votos para todas as listas, solicitará o cálculo de lugares à camada [business]. A camada [business] pode realizar este cálculo e devolver o resultado à camada [UI].
  3. A camada [ui] pode então apresentar estes resultados ao utilizador. O utilizador também pode solicitar que sejam guardados.
  4. A camada [ui] pode ainda querer apresentar informações adicionais ao utilizador, tais como o limiar eleitoral ou o número de lugares a preencher.

Com esta informação, poderíamos tentar uma definição inicial da interface para a camada [ metier]:


public interface IElectionsMetier {
 
    public ListeElectorale[] getListesElectorales();

    public int getNbSiegesAPourvoir();
 
    public double getSeuilElectoral();
 
    public void recordResultats(ListeElectorale[] listesElectorales);
 
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
 
}
  • Linha 1: A interface chama-se [IElectionsMetier]. Define os seguintes métodos:
    • linha 3: um método [getVoterLists] que permitirá à camada [ui] obter o conjunto de listas concorrentes;
    • linha 5: o método [getNbSiegesAPourvoir] recupera o número de lugares a preencher;
    • linha 7: o método [getElectoralThreshold] recupera o limiar eleitoral;
    • linha 11: um método [calculateSeats] (linha 36) que permitirá à camada [ui] solicitar o cálculo dos lugares assim que os resultados da contagem de votos das várias listas forem conhecidos. O parâmetro é a matriz de listas concorrentes, sem os seus lugares e sem o booleano «eliminated». O resultado devolvido é esta mesma matriz, mas desta vez com os campos [seats, eliminated] inicializados;
    • Linha 9: um método [recordResults] que permitirá à camada [ui] solicitar o registo dos resultados.

Nota: Devido à sua posição, a camada [business] reutiliza alguns dos métodos da camada [DAO] para os disponibilizar à camada [UI]. Devido a esta redundância, pode-se sentir a tentação de agrupar tudo numa única camada que combinasse tanto a lógica de negócio como o acesso aos dados. Esta camada única é por vezes chamada de modelo, o M na sigla MVC (Model-View-Controller). MVC é um padrão de design amplamente utilizado em aplicações web.

Vamos examinar a assinatura do método [calculateSeats]:


public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

Foi referido anteriormente: «O parâmetro é a matriz de listas concorrentes, sem os seus lugares e sem o booleano ‘eliminated’. O resultado é a mesma matriz, mas desta vez com os campos [seats, eliminated].» A assinatura do método também poderia ser a seguinte:


public void calculerSieges(ListeElectorale[] listesElectorales);

O parâmetro [voterLists] é uma referência a um objeto, neste caso uma matriz. Cada elemento é, por sua vez, uma referência a um objeto, neste caso do tipo [VoterList]. O método [calculateSeats] irá modificar os campos [seats, eliminated] de cada um desses objetos. O método chamador mantém um ponteiro [voterLists] que:

  • antes da chamada, é uma referência a uma matriz de objetos [VoterList] com os seus campos [seats, eliminated] não inicializados;
  • após a chamada, é a referência (a mesma) a uma matriz de objetos [VoterList] com os seus campos [seats, eliminated] inicializados;

Então, por que usar a assinatura:


public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

Ao escrever uma interface, é importante lembrar que ela pode ser utilizada em dois contextos diferentes: local e remoto . No contexto local, o método chamador e o método chamado são executados na mesma JVM (Java Virtual Machine):

Se a camada [ui] chamar o método calculateSeats da camada [DAO], ela tem, de facto, uma referência ao parâmetro [VoterList[] voterLists] que passa para o método.

No contexto remoto, o método chamador e o método chamado são executados em JVMs diferentes:

No exemplo acima, a camada [ui] é executada na JVM 1 e a camada [business] na JVM 2, em duas máquinas diferentes. As duas camadas não comunicam diretamente. Entre elas existe uma camada a que chamaremos de camada de comunicação [1]. Esta consiste numa camada de transmissão [2] e numa camada de receção [3]. O programador geralmente não precisa de escrever estas camadas de comunicação. Elas são geradas automaticamente por ferramentas de software. A camada [business] é escrita como se estivesse a ser executada na mesma JVM que a camada [DAO]. Por conseguinte, não são necessárias alterações no código.

O mecanismo de comunicação entre a camada [ui] e a camada [business] é o seguinte:

  • a camada [ui] chama o método calculateSeats da camada [business], passando-lhe o parâmetro [VoterList[] voterLists1];
  • este parâmetro é, na verdade, passado para a camada de transmissão [2]. Esta camada transmitirá o valor do parâmetro `listesElectorales1` pela rede, e não a sua referência. A forma exata deste valor depende do protocolo de comunicação utilizado;
  • a camada de receção [3] irá recuperar este valor e utilizá-lo para reconstruir um objeto [VoterList[] voterLists2] que espelha o parâmetro inicial enviado pela camada [business]. Temos agora dois objetos idênticos (em termos de conteúdo) em duas JVMs diferentes: voterLists1 e voterLists2.
  • A camada de receção passará o objeto listesElectorales2 para o método calculerSieges da camada [business], que o persistirá na base de dados. Após esta operação, a referência listesElectorales2 aponta para uma matriz de objetos [VoterList] com os seus campos [seats, eliminated] inicializados. Este não é o caso do objeto listesElectorales1, ao qual a camada [ui] tem uma referência. Se quisermos que a camada [ui] tenha uma referência ao objeto listesElectorales2, temos de o passar para a camada [ui]. Por isso, utilizamos a seguinte assinatura para o método [calculerSieges]:

public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
  • Com esta assinatura, o método `calculateSeats` devolverá a referência `electoralLists2` como resultado. Este resultado é devolvido à camada receptora [3], que tinha chamado a camada [business]. A camada [business] devolverá o valor (e não a referência) de `electoralLists2` à camada emissora [2];
  • A camada de envio [2] irá recuperar este valor e utilizá-lo para reconstruir um objeto [VoterList[] voterLists3] que reflete o resultado devolvido pelo método calculateSeats da camada [business].
  • O objeto [VoterList[] voterLists3] é devolvido ao método na camada [UI] cuja chamada ao método calculateSeats da camada [DAO] tinha iniciado todo este mecanismo;

Neste processo, objetos do tipo [VoterList] passarão entre as camadas [2] e [3]:

  • Quando a camada [2] transmite o valor de um objeto [VoterList] para a camada [3], diz-se que o objeto é serializado. A forma exata desta serialização depende do protocolo de comunicação utilizado;
  • Quando a camada [3] recupera o valor de um objeto [VoterList] para criar um novo objeto [VoterList], diz-se que o objeto é deserializado;

Para que um objeto seja submetido a esta serialização/desserialização, certos protocolos exigem que o objeto implemente a interface [Serializable]. Esta interface é meramente um marcador; não há métodos a implementar. Portanto, a classe [VoterList] será agora declarada da seguinte forma:


public abstract class ListeElectorale implements Serializable {
    private static final long serialVersionUID = 1L;
  • O campo na linha 2 é obrigatório. Pode ser mantido tal como está e utilizado para qualquer classe do tipo [Serializable].

4.3. A classe de exceção

Voltemos à interface da camada [DAO]:


public interface IElectionsDao {
 
  public double getSeuilElectoral();
 
  public int getNbSiegesAPourvoir();
 
  public ListeElectorale[] getListesElectorales();
 
  public void setListesElectorales(ListeElectorale[] listesElectorales);
}

Estes métodos funcionam com uma base de dados e podem encontrar vários erros, tais como a indisponibilidade da base de dados. Ao escrever um método, deve sempre tratar os casos de erro. Estes são normalmente sinalizados por uma exceção. Já nos deparámos com a classe [ElectionsException] na Secção 3.3. Continuaremos a utilizá-la, mas iremos melhorá-la da seguinte forma:


package ...;
 
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
 
// exception class for the Elections application
// the exception is uncontrolled
 
public class ElectionsException extends RuntimeException implements Serializable {
 
    // serial ID
    private static final long serialVersionUID = 1L;
 
    // local fields
    private int code;
    private List<String> erreurs;
 
    // manufacturers
    public ElectionsException() {
        super();
    }
 
    public ElectionsException(int code, Throwable e) {
        // parent
        super(e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }
 
    public ElectionsException(int code, String message, Throwable e) {
        // parent
        super(message,e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }
 
    public ElectionsException(int code, String message) {
        // parent
        super(message);
        // local
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }
 
    public ElectionsException(int code, List<String> erreurs) {
        // parent
        super();
        // local
        this.code = code;
        this.erreurs = erreurs;
    }
 
    // list of exception error messages
    private List<String> getErreursForException(Throwable th) {
        // retrieve the list of exception error messages
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
    }
 
    // getters and setters
...
}
  • linhas 16-17: o tipo [ElectionsException] encapsula:
    • um código de erro, linha 16;
    • uma lista de mensagens de erro, linha 17;

A classe suporta cinco construtores:

  • linha 20: ElectionsException()
  • Linha 24: ElectionsException(int code, Throwable e): O segundo parâmetro é do tipo [Throwable], que é a superclasse da classe [Exception]. Este construtor permite que a exceção e seja encapsulada juntamente com um código de erro. O tipo [Throwable] (e, portanto, o tipo Exception) permite encapsular uma ou mais exceções. A ideia é:
    • capturar uma exceção que ocorra;
    • enriquecê-la com uma mensagem, encapsulando-a numa nova exceção;
    • lançar a nova exceção;
try{
...
}catch (Exception1 e1){
   throw new Exception2(«un message»,e1);
}

A encapsulação ocorre na linha 34 através da instrução [super(message, e)]. Este processo de encapsulação pode ser repetido, e a exceção inicial pode ser enriquecida com mensagens diferentes. Isto é designado por pilha de exceções. O método [private List<String> getErrorsForException(Throwable th)] permite-lhe recuperar as várias mensagens associadas às exceções encapsuladas:

  • (continuação)
    • (continuação)
      • A exceção encapsulada é obtida utilizando o método Throwable [Throwable].getCause();
      • a mensagem associada a uma exceção é obtida através do método String [Throwable].getMessage();
  • linhas 28–29: os campos [code, errors] são construídos;
  • linha 32: public ElectionsException(int code, String message, Throwable e): este construtor é semelhante ao anterior, exceto que enriquece a exceção que irá encapsular com um código e uma mensagem;
  • linha 40: public ElectionsException(int code, String message): construtor sem encapsulamento de exceção;
  • linha 50: public ElectionsException(int code, List<String> errors): construtor sem encapsulamento de exceção nem mensagem;

A classe [ElectionsException] pode ser utilizada da seguinte forma:

try{
...
}catch (Exception1 e1){
   throw new ElectionsException(un_code,un_message,e1);
}

onde a mensagem pode ou não estar presente. Uma vez criada, a exceção [ElectionsException] não se destina a encapsular novas exceções. No exemplo acima, ela encapsula a exceção e1 e as exceções que e1 encapsula. Não há encapsulamentos adicionais além disso.

A classe [ElectionsException] também pode ser utilizada da seguinte forma:

// code susceptible de rencontrer un cas d'erreur (mais pas sous la forme d'une exception)
...
if(erreur){
    throw new ElectionsException(un_code,un_message);
}