6. Arquiteturas de três camadas
6.1. Introduction
Vamos revisar a última versão do aplicativo de cálculo de impostos:
using System;
namespace Chap3 {
class Program {
static void Main() {
// programa interativo de cálculo de imposto
// o usuário digita três dados no teclado: casado nbEnfants salário
// o programa exibe então o imposto a pagar
...
// criação de um objeto IImpot
IImpot impot = null;
try {
// criação de um objeto IImpot
impot = new FileImpot("DataImpotInvalide.txt");
} catch (FileImpotException e) {
// exibição de erro
...
// interrupção do programa
Environment.Exit(1);
}
// loop infinito
while (true) {
// solicitação dos parâmetros para o cálculo do imposto
Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
string paramètres = Console.ReadLine().Trim();
...
// os parâmetros estão corretos — calculando o imposto
Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
// próximo contribuinte
}//enquanto
}
}
}
A solução anterior inclui processos clássicos da programação:
- a recuperação de dados armazenados em arquivos, bancos de dados, etc. — linhas 12 a 21
- a interação com o usuário, linhas 26 (entradas) e 29 (exibições)
- o uso de um algoritmo de negócio, linha 29
A prática demonstrou que isolar esses diferentes processos em classes separadas melhora a facilidade de manutenção das aplicações. A arquitetura de uma aplicação assim estruturada é a seguinte:
![]() |
Essa arquitetura é chamada de “arquitetura de três camadas”, tradução do inglês “three-tier architecture”. O termo “três camadas” normalmente se refere a uma arquitetura em que cada camada está em uma máquina diferente. Quando as camadas estão na mesma máquina, a arquitetura passa a ser uma “arquitetura de três camadas”.
- A camada [metier] é aquela que contém as regras de negócio do aplicativo. Para o nosso aplicativo de cálculo de impostos, são as regras que permitem calcular o imposto de um contribuinte. Essa camada precisa de dados para funcionar:
- as faixas de imposto, dados que mudam a cada ano
- o número de filhos, o estado civil e o salário anual do contribuinte
No esquema acima, os dados podem vir de dois lugares:
- a camada de acesso aos dados ou [dao] (DAO = Data Access Object) para os dados já registrados em arquivos ou bancos de dados. Esse poderia ser o caso, aqui, das faixas de imposto, como foi feito na versão anterior do aplicativo.
- a camada de interface com o usuário ou [ui] (UI = Interface do Usuário) para os dados inseridos pelo usuário ou exibidos ao usuário. Esse poderia ser o caso, aqui, do número de filhos, do estado civil e do salário anual do contribuinte
- De maneira geral, a camada [dao] é responsável pelo acesso a dados persistentes (arquivos, bancos de dados) ou não persistentes (rede, sensores, etc.).
- Já a camada [ui] é responsável pelas interações com o usuário, caso haja um.
- As três camadas são tornadas independentes graças ao uso de interfaces.
Vamos retomar a aplicação [Impots], já estudada várias vezes, para lhe conferir uma arquitetura de três camadas. Para isso, vamos estudar as camadas [ui, metier, dao] uma a uma, começando pela camada [dao], que lida com os dados persistentes.
Antes disso, precisamos definir as interfaces das diferentes camadas da aplicação [Impots].
6.2. As interfaces da aplicação [Impots]
Vale lembrar que uma interface define um conjunto de assinaturas de métodos. As classes que implementam a interface fornecem um conteúdo para esses métodos.
Voltemos à arquitetura de três camadas da nossa aplicação:
![]() |
Nesse tipo de arquitetura, geralmente é o usuário quem toma a iniciativa. Ele faz uma solicitação em [1] e recebe uma resposta em [8]. Isso é chamado de ciclo solicitação-resposta. Tomemos como exemplo o cálculo do imposto de um contribuinte. Esse cálculo exigirá várias etapas:
- a camada [ui] precisará solicitar ao usuário o número de filhos, o estado civil e o salário anual. Essa é a operação [1] mencionada acima.
- Feito isso, a camada [ui] solicitará à camada de negócios que faça o cálculo do imposto. Para isso, ela transmitirá a ela os dados que recebeu do usuário. Essa é a operação [2].
- A camada [metier] precisa de certas informações para realizar seu trabalho: as faixas de imposto. Ela solicitará essas informações à camada [dao] por meio do caminho [3, 4, 5, 6]. [3] é a solicitação inicial e [6] é a resposta a essa solicitação.
- Com todos os dados de que precisava, a camada [metier] calcula o imposto.
- A camada [metier] agora pode responder à solicitação da camada [ui] feita em (b). Esse é o caminho [7].
- A camada [ui] formatará esses resultados e os apresentará ao usuário. Esse é o caminho [8].
- É possível imaginar que o usuário faça simulações de impostos e queira salvá-las. Ele utilizará o caminho [1-8] para fazer isso.
Vemos nessa descrição que uma camada utiliza os recursos da camada à sua direita, nunca da que está à sua esquerda. Consideremos duas camadas contíguas:
![]() |
A camada [A] faz solicitações à camada [B]. Nos casos mais simples, uma camada é implementada por uma única classe. Um aplicativo evolui ao longo do tempo. Assim, a camada [B] pode ter diferentes classes de implementação, como [B1, B2, ...]. Se a camada [B] for a camada [dao], esta pode ter uma primeira implementação, [B1], que busca dados em um arquivo. Alguns anos depois, pode-se querer armazenar os dados em um banco de dados. Então, criaremos uma segunda classe de implementação, [B2]. Se, na aplicação inicial, a camada [A] trabalhasse diretamente com a classe [B1], seríamos obrigados a reescrever parcialmente o código da camada [A]. Suponhamos, por exemplo, que tenhamos escrito na camada [A] algo como o seguinte:
- linha 1: é criada uma instância da classe [B1]
- linha 3: são solicitados dados a essa instância
Se supusermos que a nova classe de implementação [B2] utilize métodos com a mesma assinatura que os da classe [B1], será necessário alterar todos os [B1] para [B2]. Esse é o caso mais favorável e bastante improvável, caso não se tenha prestado atenção a essas assinaturas de métodos. Na prática, é comum que as classes [B1] e [B2] não tenham as mesmas assinaturas de métodos e que, portanto, boa parte da camada [A] precise ser totalmente reescrita.
É possível melhorar a situação se for inserida uma interface entre as camadas [A] e [B]. Isso significa que as assinaturas dos métodos apresentados pela camada [B] à camada [A] são fixadas em uma interface. O esquema anterior passa então a ser o seguinte:
![]() |
A camada [A] não se comunica mais diretamente com a camada [B], mas sim com 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]. Feito isso, é a interface [IB] — e não sua classe de implementação — que é utilizada no código. O código anterior passa a ser o seguinte:
- linha 1: uma instância [ib] que implementa a interface [IB] é criada por instanciamento da classe [B1]
- linha 3: são solicitados dados à instância [ib]
A partir de agora, se substituirmos a implementação [B1] da camada [B] por uma implementação [B2], e se ambas as implementações respeitarem a mesma interface [IB], então apenas a linha 1 da camada [A] deve ser alterada, e nenhuma outra. Essa é uma grande vantagem que, por si só, justifica o uso sistemático de interfaces entre duas camadas.
É possível ir ainda mais longe e tornar a camada [A] totalmente independente da camada [B]. No código acima, a linha 1 apresenta um problema porque faz referência direta à classe [B1]. O ideal seria que a camada [A] pudesse dispor de uma implementação da interface [IB] sem precisar nomear uma classe. Isso seria coerente com nosso esquema acima. Vemos que a camada [A] se refere à interface [IB] e não vemos por que ela precisaria saber o nome da classe que implementa essa interface. Esse detalhe não é útil para a camada [A].
O framework Spring (http://www.springframework.org) permite alcançar esse resultado. A arquitetura anterior evolui da seguinte maneira:
![]() |
A camada transversal [Spring] permitirá que uma camada obtenha, por meio de configuração, uma referência à camada localizada à sua direita, sem precisar saber o nome da classe de implementação dessa camada. Esse nome estará nos arquivos de configuração e não no código C#. O código C# da camada [A] assume, então, a seguinte forma:
- linha 1: uma instância [ib] que implementa a interface [IB] da camada [B]. Essa instância é criada pelo Spring com base em informações encontradas em um arquivo de configuração. O Spring se encarregará de criar:
- a instância [b], que implementa a camada [B]
- a instância [a], que implementa a camada [A]. Essa instância será inicializada. O campo [ib] acima receberá como valor a referência [b] do objeto que implementa a camada [B]
- linha 3: são solicitados dados à instância [ib]
Percebe-se agora que a classe de implementação [B1] da camada B não aparece em nenhum lugar no 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]. Bastará alterar os arquivos de configuração do Spring para instanciar [B2] em vez de [B1].
A combinação do Spring com as interfaces C# traz uma melhoria decisiva para a manutenção de aplicativos, tornando suas camadas independentes umas das outras. É essa solução que utilizaremos para uma nova versão do aplicativo [Impots].
Voltemos à arquitetura de três camadas do nosso aplicativo:
![]() |
Em casos simples, podemos partir da camada [metier] para descobrir as interfaces do aplicativo. Para funcionar, ele precisa de dados:
- já disponíveis em arquivos, bancos de dados ou pela rede. Esses dados são fornecidos pela camada [dao].
- ainda não disponíveis. Nesse caso, são fornecidos pela camada [ui], que os obtém do usuário do aplicativo.
Que interface a camada [dao] deve oferecer à camada [metier]? Quais são as interações possíveis entre essas duas camadas? A camada [dao] deve fornecer os seguintes dados à camada [metier]:
- as faixas de imposto
Em nosso aplicativo, a camada [dao] utiliza dados existentes, mas não cria novos. Uma definição da interface da camada [dao] poderia ser a seguinte:
using Entites;
namespace Dao {
public interface IImpotDao {
// as faixas de imposto
TrancheImpot[] TranchesImpot{get;}
}
}
- linha 3: a camada [dao] será colocada no espaço de nomes [Dao]
- linha 6: a interface IImpotDao define a propriedade TranchesImpot, que fornecerá as faixas de imposto à camada [métier].
- linha 1: importa o espaço de nomes no qual está definida a estrutura TrancheImpot:
namespace Entites {
// uma faixa de imposto
public struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
Voltemos à arquitetura de três camadas do nosso aplicativo:
![]() |
Que interface a camada [metier] deve apresentar à camada [ui]? Vamos relembrar as interações entre essas duas camadas:
- a camada [ui] solicita ao usuário o número de filhos, o estado civil e o salário anual. Essa é a operação [1] mencionada acima.
- Feito isso, a camada [ui] solicitará à camada de negócios que faça o cálculo do número de assentos. Para isso, ela transmitirá a ela os dados que recebeu do usuário. Essa é a operação [2].
Uma definição da interface da camada [metier] poderia ser a seguinte:
namespace Metier {
interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
- linha 1: colocaremos tudo o que diz respeito à camada [metier] no espaço de nomes [Metier].
- linha 2: a interface IImpotMetier define apenas um método: aquele que permite calcular o imposto de um contribuinte com base em seu estado civil, número de filhos e salário anual.
Estamos analisando uma primeira implementação dessa arquitetura em camadas.
6.3. Aplicativo de exemplo — versão 4
6.3.1. O projeto do Visual Studio
O projeto do Visual Studio será o seguinte:
![]() |
- [1]: a pasta [Entites] contém os objetos transversais às camadas [ui, metier, dao]: a estrutura TrancheImpot, a exceção FileImpotException.
- [2]: a pasta [Dao] contém as classes e interfaces da camada [dao]. Utilizaremos duas implementações da interface IImpotDao: a classe HardwiredImpot, abordada no parágrafo 4.10, e a FileImpot, abordada no parágrafo 5.8.
- [3]: a pasta [Metier] contém as classes e interfaces da camada [metier]
- [4]: a pasta [Ui] contém as classes da camada [ui]
- [5]: o arquivo [DataImpot.txt] contém as faixas de imposto utilizadas pela implementação FileImpot da camada [dao]. O [6] está configurado para ser copiado automaticamente para a pasta de execução do projeto.
6.3.2. As entidades do aplicativo
Voltemos à arquitetura de três camadas do nosso aplicativo:
![]() |
Chamamos de entités as classes transversais às camadas. Esse é geralmente o caso das classes e estruturas que encapsulam dados da camada [dao]. Essas entidades geralmente remontam até a camada [ui].
As entidades da aplicação são as seguintes:
A estrutura TrancheImpot
namespace Entites {
// uma faixa de imposto
public struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
L' (exceção) FileImpotException
using System;
namespace Entites {
public class FileImpotException : Exception {
// códigos de erro
[Flags]
public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
// código de erro
public CodeErreurs Code { get; set; }
// fabricantes
public FileImpotException() {
}
public FileImpotException(string message)
: base(message) {
}
public FileImpotException(string message, Exception e)
: base(message, e) {
}
}
}
Observação: a classe FileImpotException só é útil se a camada [dao] for implementada pela classe FileImpot.
6.3.3. A camada [dao]
![]() |
Recordemos a interface da camada [dao]:
using Entites;
namespace Dao {
public interface IImpotDao {
// faixas de imposto
TrancheImpot[] TranchesImpot{get;}
}
}
Implementaremos essa interface de duas maneiras diferentes.
Primeiramente, com a classe HardwiredImpot, analisada no parágrafo 4.10:
using System;
using Entites;
namespace Dao {
public class HardwiredImpot : IImpotDao {
// tabelas de dados necessárias para o cálculo do imposto
decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
// faixas de imposto
public TrancheImpot[] TranchesImpot { get; private set; }
// construtor
public HardwiredImpot() {
// criação da tabela de faixas de imposto
TranchesImpot = new TrancheImpot[limites.Length];
// preenchimento
for (int i = 0; i < TranchesImpot.Length; i++) {
TranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
}
}
}// classe
}// espaço de nomes
- linha 5: a classe HardwiredImpot implementa a interface IImpotDao
- linha 12: implementação da propriedade TranchesImpot da interface IImpotDao. Essa propriedade é automática. Ela implementa o método get da propriedade TranchesImpot da interface IImpotDao. Além disso, foi declarado um método set como privado, ou seja, interno à classe, para que o construtor das linhas 15 a 22 possa inicializar a matriz de faixas de imposto.
A interface IImpotDao também será implementada pela classe FileImpot, analisada no parágrafo 5.8:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Entites;
namespace Dao {
class FileImpot : IImpotDao {
// arquivo de dados
public string FileName { get; set; }
// faixas de imposto
public TrancheImpot[] TranchesImpot { get; private set; }
// construtor
public FileImpot(string fileName) {
// armazenamos o nome do arquivo
FileName = fileName;
// dados
List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
int numLigne = 1;
// exceção
FileImpotException fe = null;
// leitura do conteúdo do arquivo fileName, linha por linha
Regex pattern = new Regex(@"s*:\s*");
// inicialmente, sem erros
FileImpotException.CodeErreurs code = 0;
try {
using (StreamReader input = new StreamReader(FileName)) {
while (!input.EndOfStream && code == 0) {
// linha atual
string ligne = input.ReadLine().Trim();
// ignora-se as linhas vazias
if (ligne == "")
continue;
// linha dividida em três campos separados por:
string[] champsLigne = pattern.Split(ligne);
// existem 3 campos?
if (champsLigne.Length != 3) {
code = FileImpotException.CodeErreurs.Ligne;
}
// conversões dos 3 campos
decimal limite = 0, coeffR = 0, coeffN = 0;
if (code == 0) {
if (!Decimal.TryParse(champsLigne[0], out limite))
code = FileImpotException.CodeErreurs.Champ1;
if (!Decimal.TryParse(champsLigne[1], out coeffR))
code |= FileImpotException.CodeErreurs.Champ2;
if (!Decimal.TryParse(champsLigne[2], out coeffN))
code |= FileImpotException.CodeErreurs.Champ3;
;
}
// erro?
if (code != 0) {
// registramos o erro
fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
} else {
// memoriza-se a nova faixa de imposto
listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
// linha seguinte
numLigne++;
}
}
}
} catch (Exception e) {
// registra-se o erro
fe = new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = FileImpotException.CodeErreurs.Acces };
}
// erro a ser relatado?
if (fe != null) {
// lança-se a exceção
throw fe;
} else {
// a lista listImpot é inserida na tabela tranchesImpot
TranchesImpot = listTranchesImpot.ToArray();
}
}
}
}
- esse código já foi analisado no parágrafo 5.8.
- linha 14: o método TranchesImpot da interface IImpotDao
- linha 76: inicialização das faixas de imposto no construtor da classe, a partir do arquivo cujo nome foi passado ao construtor na linha 17.
6.3.4. A camada [metier]
![]() |
Vamos relembrar a interface dessa camada:
namespace Metier {
public interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
A implementação ImpotMetier dessa interface é a seguinte:
using Entites;
using Dao;
namespace Metier {
public class ImpotMetier : IImpotMetier {
// camada [dao]
private IImpotDao Dao { get; set; }
// faixas de imposto
private TrancheImpot[] tranchesImpot;
// construtor
public ImpotMetier(IImpotDao dao) {
// armazenamento
Dao = dao;
// faixas de imposto
tranchesImpot = dao.TranchesImpot;
}
// cálculo do imposto
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
// cálculo do número de cotas
decimal nbParts;
if (marié)
nbParts = (decimal)nbEnfants / 2 + 2;
else
nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3)
nbParts += 0.5M;
// cálculo do rendimento tributável e do quociente familiar
decimal revenu = 0.72M * salaire;
decimal QF = revenu / nbParts;
// cálculo do imposto
tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
int i = 0;
while (QF > tranchesImpot[i].Limite)
i++;
// retorno do resultado
return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
}//calcular
}//classe
}
- linha 5: a classe [Metier] implementa a interface [IImpotMetier].
- linhas 14-19: a camada [metier] deve colaborar com a camada [dao]. Portanto, ela deve ter uma referência ao objeto que implementa a interface IImpotDao. É por isso que essa referência é passada como parâmetro para o construtor.
- linha 16: a referência à camada [dao] é armazenada no campo privado da linha 8
- linha 18: a partir dessa referência, o construtor solicita a tabela de faixas de imposto e armazena uma referência a ela na propriedade privada da linha 8.
- linhas 22-41: implementação do método CalculerImpot da interface IImpotMetier. Essa implementação utiliza a tabela de faixas de imposto inicializada pelo construtor.
6.3.5. A camada [ui]
![]() |
As classes de diálogo com o usuário das versões 2 e 3 eram muito semelhantes. A da versão 2 era a seguinte:
using System;
namespace Chap2 {
public class Program {
static void Main() {
...
// criação de um objeto IImpot
IImpot impot = new HardwiredImpot();
// loop infinito
while (true) {
...
}//while
}
}
}
e a da versão 3:
using System;
namespace Chap3 {
public class Program {
static void Main() {
...
// criação de um objeto IImpot
IImpot impot = null;
try {
// criação de um objeto IImpot
impot = new FileImpot("DataImpotInvalide.txt");
} catch (FileImpotException e) {
// exibição de erro
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// interrupção do programa
Environment.Exit(1);
}
// loop infinito
while (true) {
...
}//while
}
}
}
A única diferença está na forma de instanciar o objeto do tipo IImpot, que permite o cálculo do imposto. Esse objeto corresponde, neste caso, à nossa camada [métier].
Para uma implementação [dao] com a classe HardwiredImpot, a classe de diálogo é a seguinte:
using System;
using Metier;
using Dao;
using Entites;
namespace Ui {
public class Dialogue2 {
static void Main() {
...
// criação das camadas [metier et dao]
IImpotMetier metier = new ImpotMetier(new HardwiredImpot());
// loop infinito
while (true) {
...
// os parâmetros estão corretos — calcula-se o imposto
Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
// próximo contribuinte
}//enquanto
}
}
}
- linha 12: instanciação das camadas [dao] e [metier]. Vale lembrar que a camada [metier] depende da camada [dao].
- linha 18: uso da camada [metier] para calcular o imposto
Para uma implementação [dao] com a classe FileImpot, a classe de diálogo é a seguinte:
using System;
using Metier;
using Dao;
using Entites;
namespace Ui {
public class Dialogue {
static void Main() {
...
// criando as camadas [metier et dao]
IImpotMetier metier = null;
try {
// criação da camada [metier]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (FileImpotException e) {
// exibição de erro
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// interrupção do programa
Environment.Exit(1);
}
// loop infinito
while (true) {
...
// os parâmetros estão corretos — calculando o imposto
Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
// próximo contribuinte
}//while
}
}
}
- linhas 11-21: instanciação das camadas [dao] e [metier]. Como a instanciação da camada [dao] pode gerar uma exceção, esta é tratada
- linha 26: uso da camada [metier] para calcular o imposto, como na versão anterior
6.3.6. Conclusão
A arquitetura em camadas e o uso de interfaces trouxeram certa flexibilidade à nossa aplicação. Isso fica evidente, principalmente, na maneira como a camada [ui] instancia as camadas [dao] e [métier]:
// criando as camadas [metier et dao]
IImpotMetier metier = new ImpotMetier(new HardwiredImpot());
em um caso e:
// criam-se as camadas [metier et dao]
IImpotMetier metier = null;
try {
// criação da camada [metier]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (FileImpotException e) {
// exibição de erro
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// interrupção do programa
Environment.Exit(1);
}
no outro. Exceto pelo tratamento da exceção no caso 2, a instanciação das camadas [dao] e [metier] é semelhante nas duas aplicações. Uma vez instanciadas as camadas [dao] e [metier], o código da camada [ui] é idêntico nos dois casos. Isso se deve ao fato de que a camada [métier] é manipulada por meio de sua interface IImpotMetier e não por meio da classe de implementação desta. Alterar a camada [metier] ou a camada [dao] do aplicativo sem alterar suas interfaces sempre significará apenas alterar as linhas anteriores na camada [ui].
Outro exemplo da flexibilidade proporcionada por essa arquitetura é a implementação da camada [métier]:
using Entites;
using Dao;
namespace Metier {
public class ImpotMetier : IImpotMetier {
// camada [dao]
private IImpotDao Dao { get; set; }
// faixas de imposto
private TrancheImpot[] tranchesImpot;
// fabricante
public ImpotMetier(IImpotDao dao) {
// armazenamento
Dao = dao;
// faixas de imposto
tranchesImpot = dao.TranchesImpot;
}
// cálculo do imposto
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
...
}//calcular
}//classe
}
Na linha 14, vemos que a camada [métier] é construída a partir de uma referência à interface da camada [dao]. Alterar a implementação desta última, portanto, não causa nenhum impacto na camada [métier]. É por isso que nossa única implementação da camada [métier] pôde funcionar sem modificações com duas implementações diferentes da camada [dao].
6.4. Exemplo de aplicação — versão 5
![]() |
Esta nova versão retoma a anterior, trazendo as seguintes alterações:
- as camadas [métier] e [dao] estão, cada uma, encapsuladas em uma DLL e testadas com o framework de testes unitários NUnit.
- A integração das camadas é garantida pela estrutura Spring
Em grandes projetos, vários desenvolvedores trabalham no mesmo projeto. As arquiteturas em camadas facilitam essa forma de trabalho: como as camadas se comunicam entre si por meio de interfaces bem definidas, um desenvolvedor que trabalha em uma camada não precisa se preocupar com o trabalho dos outros desenvolvedores nas demais camadas. Basta que todos respeitem as interfaces.
No exemplo acima, o desenvolvedor da camada [métier] precisará, no momento dos testes de sua camada, de uma implementação da camada [dao]. Enquanto essa implementação não estiver concluída, ele pode utilizar uma implementação fictícia da camada [dao], desde que ela respeite a interface IImpotDao. Essa também é uma vantagem da arquitetura em camadas: um atraso na camada [dao] não impede os testes da camada [métier]. A implementação simulada da camada [dao] também tem a vantagem de, muitas vezes, ser mais fácil de implementar do que a camada real [dao], que pode exigir a execução de um SGBD, conexões de rede, etc.
Quando a camada [dao] estiver concluída e testada, ela será fornecida aos desenvolvedores da camada [métier] na forma de um DLL, em vez de código-fonte. No final, o aplicativo é frequentemente entregue na forma de um executável .exe (o da camada [ui]) e de bibliotecas de classes .dll (as demais camadas).
6.4.1. NUnit
Os testes realizados até agora para nossas diversas aplicações baseavam-se em uma verificação visual. Verificava-se se o que aparecia na tela correspondia ao esperado. Esse método é inviável quando há muitos testes a serem realizados. O ser humano está, de fato, sujeito à fadiga e sua capacidade de verificar testes diminui ao longo do dia. Os testes devem, portanto, ser automatizados e ter como objetivo não exigir nenhuma intervenção humana.
Um aplicativo evolui com o tempo. A cada atualização, é preciso verificar se o aplicativo não sofre “regressão”, c.a.d, e se continua a passar nos testes de funcionamento que foram realizados durante sua criação inicial. Esses testes são chamados de testes de “não regressão”. Um aplicativo de certo porte pode exigir centenas de testes. De fato, testa-se cada método de cada classe da aplicação. Isso é chamado de testes unitários. Esses testes podem envolver muitos desenvolvedores se não tiverem sido automatizados.
Foram desenvolvidas ferramentas para automatizar os testes. Uma delas se chama NUnit. Ela está disponível no site [http://www.nunit.org]:
![]() | ![]() |
Foi utilizada a versão 2.4.6 acima para este documento (março de 2008). A instalação coloca um ícone [1] na área de trabalho:
![]() |
Um clique duplo no ícone [1] inicia a interface gráfica do NUnit [2]. Essa interface não contribui em nada para a automação dos testes, já que, mais uma vez, somos levados a uma verificação visual: o testador verifica os resultados dos testes exibidos na interface gráfica. No entanto, os testes também podem ser executados por ferramentas em lote e seus resultados registrados em arquivos XML. É esse método que é utilizado pelas equipes de desenvolvimento: os testes são executados à noite e os desenvolvedores recebem o resultado na manhã seguinte.
Vamos examinar, com um exemplo, o princípio dos testes NUnit. Primeiro, vamos criar um novo projeto C# do tipo Console Application:
![]() |
No [1], vemos as referências do projeto. Essas referências são références que contêm classes e interfaces utilizadas pelo projeto. As apresentadas em [1] são incluídas por padrão em cada novo projeto C#. Para podermos utilizar as classes e interfaces do framework NUnit, precisamos adicionar uma nova referência ao projeto.
![]() |
Na guia .NET acima, selecionamos o componente [nunit.framework]. Os componentes [nunit.*] acima não estão presentes por padrão no ambiente .NET. Eles foram adicionados por meio da instalação anterior do framework NUnit. Após a validação da adição da referência, ela aparece como [4] na lista de referências do projeto.
Antes da geração do aplicativo, a pasta [bin/Release] do projeto está vazia. Após a geração (F6), é possível observar que a pasta [bin/Release] não está mais vazia:
![]() |
Em [6], observa-se a presença de DLL e [nunit.framework.dll]. Foi a adição da referência [nunit.framework] que provocou a cópia desse DLL na pasta de execução. Essa pasta é, de fato, uma das que serão exploradas pelo CLR (Common Language Runtime) .NET para localizar as classes e interfaces referenciadas pelo projeto.
Vamos criar uma primeira classe de teste, NUnit. Para isso, excluímos a classe [Program.cs] gerada por padrão e, em seguida, adicionamos uma nova classe, [Nunit1.cs], ao projeto. Também excluímos as referências desnecessárias [7].
A classe de teste NUnit1 ficará da seguinte forma:
using System;
using NUnit.Framework;
namespace NUnit {
[TestFixture]
public class NUnit1 {
public NUnit1() {
Console.WriteLine("constructeur");
}
[SetUp]
public void avant() {
Console.WriteLine("Setup");
}
[TearDown]
public void après() {
Console.WriteLine("TearDown");
}
[Test]
public void t1() {
Console.WriteLine("test1");
Assert.AreEqual(1, 1);
}
[Test]
public void t2() {
Console.WriteLine("test2");
Assert.AreEqual(1, 2, "1 n'est pas égal à 2");
}
}
}
- linha 6: a classe NUnit1 deve ser pública. A palavra-chave public não é gerada por padrão pelo Visual Studio. É preciso adicioná-la.
- linha 5: o atributo [TestFixture] é um atributo NUnit. Ele indica que a classe é uma classe de teste.
- linhas 7-9: o construtor. Ele é usado aqui apenas para exibir uma mensagem na tela. Queremos ver quando ele é executado.
- linha 10: o atributo [SetUp] define um método executado antes de cada teste unitário.
- linha 14: o atributo [TearDown] define um método executado após cada teste unitário.
- linha 18: o atributo [Test] define um método de teste. Para cada método anotado com o atributo [Test], o método anotado [SetUp] será executado antes do teste e o método anotado [TearDown] será executado após o teste.
- linha 21: um dos métodos [Assert.*] definidos pelo framework NUnit. Encontram-se os seguintes métodos [Assert]:
- [Assert.AreEqual(expression1, expression2)]: verifica se os valores das duas expressões são iguais. São aceitos diversos tipos de expressão (int, string, float, double, decimal, ...). Se as duas expressões não forem iguais, uma exceção é lançada.
- [Assert.AreEqual(réel1, réel2, delta)]: verifica se dois números reais são iguais com uma tolerância de delta, c.a.d abs(real1-real2)<=delta. Pode-se escrever, por exemplo, [Assert.AreEqual(réel1, réel2, 1E-6)] para verificar se dois valores são iguais com uma tolerância de 10⁻⁶.
- [Assert.AreEqual(expression1, expression2, message)] e [Assert.AreEqual(réel1, réel2, delta, message)] são variantes que permitem especificar a mensagem de erro a ser associada à exceção lançada quando o método [Assert.AreEqual] falha.
- [Assert.IsNotNull(object)] e [Assert.IsNotNull(object, message)]: verifica se object não é igual a null.
- [Assert.IsNull(object)] e [Assert.IsNull(object, message)]: verificam se object é igual a null.
- [Assert.IsTrue(expression)] e [Assert.IsTrue(expression, message)]: verifica se a expressão é igual a true.
- [Assert.IsFalse(expression)] e [Assert.IsFalse(expression, message)]: verifica se a expressão é igual a false.
- [Assert.AreSame(object1, object2)] e [Assert.AreSame(object1, object2, message)]: verifica se as referências object1 e object2 apontam para o mesmo objeto.
- [Assert.AreNotSame(object1, object2)] e [Assert.AreNotSame(object1, object2, message)]: verifica se as referências object1 e object2 não apontam para o mesmo objeto.
- linha 21: a asserção deve ser bem-sucedida
- linha 26: a asserção deve falhar
Vamos configurar o projeto para que sua geração produza um DLL em vez de um executável .exe:
![]() |
- em [1]: propriedades do projeto
- em [2, 3]: como tipo de projeto, escolhe-se [Class Library] (Biblioteca de classes)
- em [4]: a geração do projeto produzirá um DLL (assembly) chamado [Nunit.dll]
Vamos agora utilizar o NUnit para executar a classe de teste:
![]() |
- em [1]: abertura de um projeto NUnit
- em [2, 3]: carregamos o bin/Release/Nunit.dll gerado pela geração do projeto C#
- em [4]: o DLL foi carregado
- em [5]: a árvore de testes
- em [6]: eles são executados
![]() |
- em [7]: os resultados: t1 foi aprovado, t2 foi reprovado
- em [8]: uma barra vermelha indica a falha geral da classe de testes
- em [9]: a mensagem de erro relacionada ao teste com falha
![]() |
- em [11]: as diferentes abas da janela de resultados
- em [12]: a aba [Console.Out]. Nela, pode-se observar que:
- o construtor foi executado apenas uma vez
- o método [SetUp] foi executado antes de cada um dos dois testes
- o método [TearDown] foi executado após cada um dos dois testes
É possível especificar os métodos a serem testados:
![]() |
- no [1]: solicita-se a exibição de uma caixa de seleção ao lado de cada teste
- no [2]: marca-se os testes a serem executados
- em [3]: eles são executados
Para corrigir os erros, basta corrigir o projeto C# e regenerá-lo. O NUnit detecta que o DLL que ele está testando foi alterado e carrega o novo automaticamente. Basta, então, reiniciar os testes.
Consideremos a seguinte nova classe de teste:
using System;
using NUnit.Framework;
namespace NUnit {
[TestFixture]
public class NUnit2 : AssertionHelper {
public NUnit2() {
Console.WriteLine("constructeur");
}
[SetUp]
public void avant() {
Console.WriteLine("Setup");
}
[TearDown]
public void après() {
Console.WriteLine("TearDown");
}
[Test]
public void t1() {
Console.WriteLine("test1");
Expect(1, EqualTo(1));
}
[Test]
public void t2() {
Console.WriteLine("test2");
Expect(1, EqualTo(2), "1 n'est pas égal à 2");
}
}
}
A partir da versão 2.4 do NUnit, uma nova sintaxe ficou disponível, como nas linhas 21 e 26. Para isso, a classe de teste deve derivar da classe AssertionHelper (linha 6).
A correspondência (não exaustiva) entre a sintaxe antiga e a nova é a seguinte:
Vamos adicionar o seguinte teste à classe NUnit2:
[Test]
public void t3() {
bool vrai = true, faux = false;
Expect(vrai, True);
Expect(faux, False);
Object obj1 = new Object(), obj2 = null, obj3=obj1;
Expect(obj1, Not.Null);
Expect(obj2, Null);
Expect(obj3, SameAs(obj1));
double d1 = 4.1, d2 = 6.4, d3 = d1;
Expect(d1, EqualTo(d3).Within(1e-6));
Expect(d1, Not.EqualTo(d2));
}
Se gerarmos (F6) o novo DLL do projeto C#, o projeto NUnit passa a ter a seguinte forma:
![]() |
- em [1]: a nova classe de teste [NUnit2] foi detectada automaticamente
- em [2]: o teste t3 de NUnit2 está sendo executado
- em [3]: o teste t3 foi aprovado
Para saber mais sobre o NUnit, consulte a ajuda do NUnit:
![]() | ![]() |
6.4.2. A solução do Visual Studio
![]() |
Vamos construir, passo a passo, a seguinte solução do Visual Studio:
![]() |
- em [1]: a solução ImpotsV5 é composta por três projetos, um para cada uma das três camadas do aplicativo
- em [2]: o projeto [dao] da camada [dao]
- em [3]: o projeto [metier] da camada [metier]
- em [4]: o projeto [ui] da camada [ui]
A solução ImpotsV5 pode ser construída da seguinte maneira:
1 ![]() | 234 ![]() | 5 ![]() |
- em [1]: criar um novo projeto
- em [2]: selecionar um aplicativo de console
- em [3]: chamar o projeto [dao]
- em [4]: criar o projeto
- em [5]: após criar o projeto, salvá-lo
![]() |
- em [6]: manter o nome [dao] para o projeto
- em [7]: especificar uma pasta para salvar o projeto e sua solução
- em [8]: atribuir um nome à solução
- em [9]: indicar que a solução deve ter sua própria pasta
- em [10]: salvar o projeto e sua solução
- em [11]: o projeto [dao] em sua solução ImpotsV5
![]() |
- em [12]: a pasta da solução ImpotsV5. Ela contém a pasta [dao] da pasta [dao].
- em [13]: o conteúdo da pasta [dao]
- em [14]: adiciona-se um novo projeto à solução ImpotsV5
![]() |
- em [15]: o novo projeto se chama [metier]
- em [16]: a solução com seus dois projetos
- em [17]: a solução, após a adição do terceiro projeto [ui]
![]() |
- em [18]: a pasta da solução e as pastas dos três projetos
- ao executar uma solução por meio de (Ctrl+F5), é o projeto ativo que é executado. O mesmo ocorre ao gerar (F6) a solução. O nome do projeto ativo aparece em negrito [19] na solução.
- em [20]: para alterar o projeto ativo da solução
- em [21]: o projeto [metier] é agora o projeto ativo da solução
6.4.3. A camada [dao]
![]() |
![]() |
As referências do projeto (ver [1] no projeto)
Adicionamos a referência [nunit.framework] necessária para os testes [NUnit]
As entidades (ver [2] no projeto)
A classe [TrancheImpot] é a das versões anteriores. A classe [FileImpotException] da versão anterior é renomeada para [ImpotException] para torná-la mais genérica e não vinculá-la a uma camada [dao] específica:
using System;
namespace Entites {
public class ImpotException : Exception {
// código de erro
public int Code { get; set; }
// fabricantes
public ImpotException() {
}
public ImpotException(string message)
: base(message) {
}
public ImpotException(string message, Exception e)
: base(message, e) {
}
}
}
A camada [dao] (ver [3] no projeto)
A interface [IImpotDao] é a da versão anterior. O mesmo se aplica à classe [HardwiredImpot]. A classe [FileImpot] foi atualizada para refletir a alteração da exceção [FileImpotException] para [ImpotException]:
...
namespace Dao {
public class FileImpot : IImpotDao {
// códigos de erro
[Flags]
public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
...
// fabricante
public FileImpot(string fileName) {
// o nome do arquivo é armazenado
FileName = fileName;
...
// inicialmente, sem erro
CodeErreurs code = 0;
try {
using (StreamReader input = new StreamReader(FileName)) {
while (!input.EndOfStream && code == 0) {
...
// erro?
if (code != 0) {
// registra-se o erro
fe = new ImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = (int)code };
} else {
...
}
}
}
} catch (Exception e) {
// registra-se o erro
fe = new ImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = (int)CodeErreurs.Acces };
}
// erro a ser relatado?
...
}
}
}
- linha 8: os códigos de erro que antes estavam na classe [FileImpotException] foram migrados para a classe [FileImpot]. Trata-se, de fato, de códigos de erro específicos dessa implementação da interface [IImpotDao].
- linhas 26 e 34: para encapsular um erro, utiliza-se a classe [ImpotException], e não mais a classe [FileImpotException].
O teste [Test1] (ver [4] no projeto)
A classe [Test1] limita-se a exibir as faixas de imposto na tela:
using System;
using Dao;
using Entites;
namespace Tests {
class Test1 {
static void Main() {
// cria-se a camada [dao]
IImpotDao dao = null;
try {
// criação da camada [dao]
dao = new FileImpot("DataImpot.txt");
} catch (ImpotException e) {
// exibição de erro
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// interrupção do programa
Environment.Exit(1);
}
// exibição das faixas de imposto
TrancheImpot[] tranchesImpot = dao.TranchesImpot;
foreach (TrancheImpot t in tranchesImpot) {
Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
}
}
}
}
- linha 13: a camada [dao] é implementada pela classe [FileImpot]
- linha 14: gerencia-se a exceção do tipo [ImpotException] que pode ocorrer.
O arquivo [DataImpot.txt], necessário para os testes, é copiado automaticamente para a pasta de execução do projeto (veja [5] no projeto). O projeto [dao] terá várias classes contendo um método [Main]. Portanto, é necessário indicar explicitamente a classe a ser executada quando o usuário solicitar a execução do projeto pressionando Ctrl-F5:
![]() |
- em [1]: acessar as propriedades do projeto
- em [2]: especificar que se trata de um aplicativo de console
- em [3]: especificar a classe a ser executada
A execução da classe [Test1] anterior produz os seguintes resultados:
4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54
O teste [Test2] (ver [4] no projeto)
A classe [Test2] faz o mesmo que a classe [Test1], implementando a camada [dao] com a classe [HardwiredImpot]. A linha 13 de [Test1] é substituída pela seguinte:
dao = new HardwiredImpot();
O projeto é modificado para agora executar a classe [Test2]:
![]() |
Os resultados na tela são os mesmos de antes.
O teste NUnit [NUnit1] (ver [4] no projeto)
O teste unitário [NUnit1] é o seguinte:
using System;
using Dao;
using Entites;
using NUnit.Framework;
namespace Tests {
[TestFixture]
public class NUnit1 : AssertionHelper{
// camada [dao] a ser testada
private IImpotDao dao;
// fabricante
public NUnit1() {
// inicialização da camada [dao]
dao = new FileImpot("DataImpot.txt");
}
// teste
[Test]
public void ShowTranchesImpot(){
// exibição das faixas de imposto
TrancheImpot[] tranchesImpot = dao.TranchesImpot;
foreach (TrancheImpot t in tranchesImpot) {
Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
}
// alguns testes
Expect(tranchesImpot.Length,EqualTo(7));
Expect(tranchesImpot[2].Limite,EqualTo(14753));
Expect(tranchesImpot[2].CoeffR, EqualTo(0.191));
Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92));
}
}
}
- a classe de teste deriva da classe [AssertionHelper], o que permite o uso do método estático Expect (linhas 27-30).
- linha 10: uma referência à camada [dao]
- linhas 13-16: o construtor instancia a camada [dao] com a classe [FileImpot]
- linhas 19-20: o método de teste
- linha 22: recupera-se a tabela de faixas de imposto da camada [dao]
- linhas 23-25: elas são exibidas como anteriormente. Essa exibição não seria necessária em um teste unitário real. Aqui, ela tem uma finalidade didática.
- linha 27: verifica-se se há, de fato, 7 faixas de imposto
- linhas 28-30: verifica-se os valores da faixa de imposto nº 2
Para executar este teste unitário, o projeto deve ser do tipo [Class Library]:
![]() |
- em [1]: a natureza do projeto foi alterada
- em [2]: o DLL gerado passará a se chamar [ImpotsV5-dao.dll]
- em [3]: após a geração (F6) do projeto, a pasta [dao/bin/Release] contém o DLL e o [ImpotsV5-dao.dll]
Em seguida, o DLL [ImpotsV5-dao.dll] é carregado no framework NUnit e executado:
![]() |
- no [1]: os testes foram bem-sucedidos. Consideramos agora a camada [dao] operacional. Seu DLL contém todas as classes do projeto, incluindo as classes de teste. Estas são desnecessárias. Estamos reconstruindo a DLL para excluir as classes de teste.
- em [2]: a pasta [tests] é excluída do projeto
- em [3]: o novo projeto. Este é regenerado pelo F6 para gerar um novo DLL.
6.4.4. A camada [metier]
![]() |
![]() |
- em [1], o projeto [metier] tornou-se o projeto ativo da solução
- em [2]: as referências do projeto
- em [3]: a camada [metier]
- em [4]: as classes de teste
- em [5]: o arquivo [DataImpot.txt] das faixas de imposto configurado em [6] para ser copiado automaticamente na pasta de execução do projeto [7]
As referências do projeto (ver [2] no projeto)
Assim como no projeto [dao], adiciona-se a referência [nunit.framework] necessária para os testes [NUnit]. A camada [metier] depende da camada [dao]. Portanto, ela precisa de uma referência à camada DLL. Proceda da seguinte forma:
![]() |
- em [1]: adiciona-se uma nova referência às referências do projeto [metier]
- em [2]: seleciona-se a aba [Browse]
- em [3]: seleciona-se a pasta [dao/bin/Release]
- em [4]: seleciona-se o DLL [ImpotsV5-dao.dll] gerado no projeto [dao]
- em [5]: a nova referência
A camada [metier] (ver [3] no projeto)
A interface [IImpotMetier] é a da versão anterior. O mesmo se aplica à classe [ImpotMetier].
O teste [Test1] (ver [4] no projeto)
A classe [Test1] limita-se a realizar alguns cálculos salariais:
using System;
using Dao;
using Entites;
using Metier;
namespace Tests {
class Test1 {
static void Main() {
// é criada a camada [metier]
IImpotMetier metier = null;
try {
// criação da camada [metier]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (ImpotException e) {
// exibição de erro
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// interrupção do programa
Environment.Exit(1);
}
// cálculo de alguns impostos
Console.WriteLine(String.Format("Impot(true,2,60000)={0} euros", metier.CalculerImpot(true, 2, 60000)));
Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 60000)));
Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 6000)));
Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 600000)));
}
}
}
- linha 14: criação das camadas [metier] e [dao]. A camada [dao] é implementada com a classe [FileImpot]
- linhas 12-21: tratamento de uma possível exceção do tipo [ImpotException]
- linhas 23-26: chamadas repetidas do único método CalculerImpot da interface [IImpotMetier].
O projeto [metier] está configurado da seguinte forma:
![]() |
- [1]: o projeto é do tipo aplicativo de console
- [2]: a classe executada é a classe [Test1]
- [3]: a geração do projeto produzirá o executável [ImpotsV5-metier.exe]
A execução do projeto produz os seguintes resultados:
O teste [NUnit1] (ver [4] no projeto)
A classe de testes unitários [NUnit1] retoma os quatro cálculos anteriores e verifica seus resultados:
using Dao;
using Metier;
using NUnit.Framework;
namespace Tests {
[TestFixture]
public class NUnit1:AssertionHelper {
// camada [metier] a ser testada
private IImpotMetier metier;
// fabricante
public NUnit1() {
// inicialização da camada [metier]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
}
// teste
[Test]
public void CalculsImpot(){
// exibição das faixas de imposto
Expect(metier.CalculerImpot(true, 2, 60000), EqualTo(4282));
Expect(metier.CalculerImpot(false, 3, 60000), EqualTo(4282));
Expect(metier.CalculerImpot(false, 3, 6000), EqualTo(0));
Expect(metier.CalculerImpot(false, 3, 600000), EqualTo(179275));
}
}
}
- linha 14: criação das camadas [metier] e [dao]. A camada [dao] é implementada com a classe [FileImpot]
- linhas 21-24: chamadas repetidas do único método CalculerImpot da interface [IImpotMetier] com verificação dos resultados.
O projeto [metier] está agora configurado da seguinte forma:
![]() |
- [1]: o projeto é do tipo “biblioteca de classes”
- [2]: a geração do projeto produzirá o DLL [ImpotsV5-metier.dll]
O projeto é gerado (F6). Em seguida, o DLL, [ImpotsV5-metier.dll] e gerados são carregados no NUnit e testados:
![]() |
Acima, os testes foram bem-sucedidos. Consideramos agora a camada [metier] operacional. Sua DLL contém todas as classes do projeto, incluindo as classes de teste. Estas são desnecessárias. Estamos reconstruindo a DLL para excluir as classes de teste.
![]() |
- em [1]: a pasta [tests] é excluída do projeto
- em [2]: o novo projeto. Este é regenerado pelo F6 para gerar um novo DLL.
6.4.5. A camada [ui]
![]() |
![]() |
- em [1], o projeto [ui] tornou-se o projeto ativo da solução
- em [2]: as referências do projeto
- em [3]: a camada [ui]
- em [4]: o arquivo [DataImpot.txt] das faixas de imposto, configurado em [5] para ser copiado automaticamente para a pasta de execução do projeto [6]
As referências do projeto (ver [2] no projeto)
A camada [ui] necessita das camadas [metier] e [dao] para realizar seus cálculos de imposto. Portanto, ela precisa de uma referência às camadas DLL dessas duas camadas. Proceda da mesma forma que foi mostrado para a camada [metier]
A classe principal [Dialogue.cs] (ver [3] no projeto)
A classe [Dialogue.cs] é a da versão anterior.
Testes
O projeto [ui] está configurado da seguinte forma:
![]() |
- [1]: o projeto é do tipo “aplicativo de console”
- [2]: a geração do projeto produzirá o executável [ImpotsV5-ui.exe]
- [3]: a classe que será executada
Um exemplo de execução (Ctrl+F5) é o seguinte:
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros
6.4.6. A camada [Spring]
Voltemos ao código em [Dialogue.cs], que cria as camadas [dao] e [metier]:
// criação das camadas [metier et dao]
IImpotMetier metier = null;
try {
// criação de camada [metier]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (ImpotException e) {
// exibição de erro
...
// interrupção do programa
Environment.Exit(1);
}
A linha 5 cria as camadas [dao] e [metier], nomeando explicitamente as classes de implementação das duas camadas: FileImpot para a camada [dao], ImpotMetier para a camada [metier]. Se a implementação de uma das camadas for feita com uma nova classe, a linha 5 será alterada. Por exemplo:
metier = new ImpotMetier(new HardwiredImpot());
Além dessa alteração, nada mudará na aplicação, pois cada camada se comunica com a seguinte por meio de uma interface. Enquanto essa interface não mudar, a comunicação entre as camadas também não mudará. O framework Spring nos permite ir um pouco mais longe na independência das camadas, permitindo externalizar em um arquivo de configuração os nomes das classes que implementam as diferentes camadas. Alterar a implementação de uma camada equivale, então, a alterar um arquivo de configuração. Não há nenhum impacto no código da aplicação.
![]() |
Acima, a camada [ui] solicitará ao Spring queinstanciar as camadas [dao], [1], [metier] e [2] com base nas informações contidas em um arquivo de configuração. A camada [ui] solicitará então ao Spring [3] uma referência à camada [metier]:
// criação das camadas [metier et dao]
IImpotMetier metier = null;
try {
// contexto Spring
IApplicationContext ctx = ContextRegistry.GetContext();
// solicitação de referência na camada [metier]
metier = (IImpotMetier)ctx.GetObject("metier");
} catch (Exception e1) {
...
}
- linha 5: instanciação das camadas [dao] e [metier] pelo Spring
- linha 7: obtém-se uma referência à camada [metier]. Observe-se que a camada [ui] obteve essa referência sem fornecer o nome da classe que implementa a camada [metier].
O framework Spring existe em duas versões: Java e .NET. A versão .NET está disponível no endereço (março de 2008) [http://www.springframework.net/]:
![]() |
- em [1]: o site de [Spring.net]
- em [2]: a página de downloads
![]() |
- em [3]: baixar o Spring 1.1 (março de 2008)
![]() |
- em [4]: baixar a versão .exe e instalá-la
- em [5]: a pasta gerada pela instalação
- em [6]: a pasta [bin/net/2.0/release] contém os arquivos DLL do Spring para projetos do Visual Studio .NET 2.0 ou superior. O Spring é um framework abrangente. O recurso do Spring que utilizaremos aqui para gerenciar a integração das camadas em uma aplicação é chamado de IoC: Inversão de Controle ou ainda DI: Injeção de Dependências. O Spring oferece bibliotecas para acesso a bancos de dados com NHibernate, geração e operação de serviços web, aplicações web, etc.
- os DLL necessários para gerenciar a integração das camadas em uma aplicação são os DLL, [7] e [8].
Armazenamos esses três DLL em uma pasta [lib] do nosso projeto:
![]() |
- [1]: os três arquivos DLL são colocados na pasta [lib] usando o Explorador do Windows
- [2]: no projeto [ui], exibimos todos os arquivos
- [3]: a pasta [ui/lib] agora está visível. Ela é incluída no projeto
- [4]: a pasta [ui/lib] faz parte do projeto
A criação da pasta [lib] não é de forma alguma indispensável. As referências poderiam ter sido criadas diretamente nas três pastas DLL da pasta [bin/net/2.0/release] de [Spring.net]. A criação da pasta [lib] permite, no entanto, desenvolver a aplicação em uma estação de trabalho que não disponha do [Spring.net], tornando-a, assim, menos dependente do ambiente de desenvolvimento disponível.
Adicionamos ao projeto [ui] referências às três novas pastas DLL:
![]() |
- [1]: criamos referências aos três DLL da pasta [lib] [2]
- [3]: os três DLL fazem parte das referências do projeto
Voltemos a uma visão geral da arquitetura do aplicativo:
![]() |
Acima, a camada [ui] solicitará ao Spring que instancie a camada [0]instanciar as camadas [dao], [1], [metier] e [2] com base nas informações contidas em um arquivo de configuração. A camada [ui] solicitará então ao Spring [3] uma referência à camada [metier]. Isso se traduzirá na camada [ui] pelo código a seguir:
// são criadas as camadas [metier et dao]
IImpotMetier metier = null;
try {
// contexto Spring
IApplicationContext ctx = ContextRegistry.GetContext();
// solicita-se uma referência à camada [metier]
metier = (IImpotMetier)ctx.GetObject("metier");
} catch (Exception e1) {
...
}
- linha 5: instanciação das camadas [dao] e [metier] pelo Spring
- linha 7: obtém-se uma referência à camada [metier].
A linha [5] acima utiliza o arquivo de configuração [App.config] do projeto do Visual Studio. Em um projeto C#, esse arquivo serve para configurar o aplicativo. [App.config], portanto, não é um conceito do Spring, mas sim do Visual Studio que o Spring utiliza. O Spring sabe utilizar outros arquivos de configuração além do [App.config]. A solução apresentada aqui, portanto, não é a única disponível.
Vamos criar o arquivo [App.config] com o assistente do Visual Studio:
![]() |
- em [1]: adição de um novo elemento ao projeto
- em [2]: selecionar “Application Configuration File”
- em [3]: [App.config] é o nome padrão desse arquivo de configuração
- em [4]: o arquivo [App.config] foi adicionado ao projeto
O conteúdo do arquivo [App.config] é o seguinte:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>
[App.config] é um arquivo XML. A configuração do projeto é definida entre as tags <configuration>. A configuração necessária para o Spring é a seguinte:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
<constructor-arg index="0" value="DataImpot.txt"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV5-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- linhas 11-23: a seção delimitada pela tag <spring> é chamada de grupo de seções <spring>. É possível criar quantos grupos de seções forem desejados no [App.config].
- Um grupo de seções contém seções: é o caso aqui:
- linhas 12-14: a seção <spring/context>
- linhas 15-22: a seção <spring/objects>
- linhas 4-9: a região <configSections> define a lista de manipuladores (handlers) dos grupos de seções presentes em [App.config].
- linhas 5-8: definem a lista de manipuladores das seções do grupo <spring> (name="spring").
- linha 6: o manipulador da seção <context> do grupo <spring>:
- name: nome da seção gerenciada
- type: nome da classe que gerencia a seção no formato NomClasse, NomDLL.
- a seção <context> do grupo <spring> é gerenciada pela classe [Spring.Context.Support.ContextHandler], que será encontrada em DLL e [Spring.Core.dll]
- linha 7: o gerenciador da seção <objects> do grupo <spring>
As linhas 4 a 9 são padrão em um arquivo [App.config] com Spring. Basta copiá-las de um projeto para outro.
- linhas 12-14: definem a seção <spring/context>.
- linha 13: a tag <resource> tem como objetivo indicar onde se encontra o arquivo que define as classes que o Spring deve instanciar. Elas podem estar no [App.config], como neste caso, mas também podem estar em outro arquivo de configuração. A localização dessas classes é definida no atributo uri da tag <resource>:
- <resource uri="config://spring/objects> indica que a lista de classes a serem instanciadas está no arquivo [App.config] (config:), na seção //spring/objects, c.a.d, dentro da tag <objects> da tag <spring>.
- <resource uri="file://spring-config.xml"> indicaria que a lista de classes a serem instanciadas se encontra no arquivo [spring-config.xml]. Esse arquivo deve ser colocado nas pastas de execução (bin/Release ou bin/Debug) do projeto. O mais simples é colocá-lo, assim como foi feito com o arquivo [DataImpot.txt], na raiz do projeto com a propriedade [Copy to output directory=always].
As linhas 12 a 14 são padrão em um arquivo [App.config] com Spring. Basta copiá-las de um projeto para outro.
- linhas 15-22: definem as classes a serem instanciadas. É nesta parte que é feita a configuração específica de uma aplicação. A tag <objects> delimita a seção de definição das classes a serem instanciadas.
- linhas 16-18: definem a classe a ser instanciada para a camada [dao]
- linha 16: cada objeto instanciado pelo Spring é objeto de uma tag <object>. Essa tag possui um atributo name, que é o nome do objeto instanciado. É por meio dele que a aplicação solicita uma referência ao Spring: “forneça-me uma referência ao objeto chamado dao”. O atributo type define a classe a ser instanciada no formato NomClasse, NomDLL. Assim, a linha 16 define um objeto chamado “dao”, instância da classe “Dao.FileImpot”, que se encontra no pacote “DLL” “ImpotsV5-dao.dll”. Observe-se que é fornecido o nome completo da classe (incluindo o espaço de nomes) e que o sufixo .dll não é especificado no nome da DLL.
Uma classe pode ser instanciada de duas maneiras com o Spring:
- por meio de um construtor específico ao qual são passados parâmetros: é o que é feito nas linhas 16 a 18.
- por meio do construtor padrão, sem parâmetros. O objeto é então inicializado por meio de suas propriedades públicas: a tag <object> possui, nesse caso, sub-tags <property> para inicializar essas diferentes propriedades. Não temos um exemplo desse caso aqui.
- (continuação)
- linha 16: a classe instanciada é a classe FileImpot. Ela possui o seguinte construtor:
public FileImpot(string fileName);
Os parâmetros do construtor são definidos por meio das tags <constructor-arg>.
- linha 17: define o primeiro e único parâmetro do construtor. O atributo index é o número do parâmetro do construtor, e o atributo value é seu valor: <constructor-arg index="i" value="valuei"/>
- linhas 19-21: definem a classe a ser instanciada para a camada [metier]: a classe [Metier.ImpotMetier], que se encontra na DLL [ImpotsV5-metier.dll].
- linha 19: a classe instanciada é a classe ImpotMetier. Ela possui o seguinte construtor:
public ImpotMetier(IImpotDao dao);
- (continuação)
- linha 20: define o primeiro e único parâmetro do construtor. Acima, o parâmetro dao do construtor é uma referência de objeto. Nesse caso, na tag <constructor-arg>, utiliza-se o atributo ref em vez do atributo value, que foi utilizado para a camada [dao]: <constructor-arg index="i" ref="refi"/>. No construtor acima, o parâmetro dao representa uma instância na camada [dao]. Essa instância foi definida pelas linhas 16 a 18 do arquivo de configuração. Assim, na linha 20:
<constructor-arg index="0" ref="dao"/>
ref="dao" representa o objeto Spring “dao” definido nas linhas 16 a 18.
Resumindo, o arquivo [App.config]:
- instancia a camada [dao] com a classe FileImpot, que recebe como parâmetro DataImpot.txt (linhas 16 a 18). O objeto resultante é chamado de “dao”
- instancia a camada [metier] com a classe ImpotMetier, que recebe como parâmetro o objeto “dao” anterior (linhas 19-21).
Resta-nos apenas utilizar esse arquivo de configuração do Spring na camada [ui]. Para isso, duplicamos a classe [Dialogue.cs] como [Dialogue2.cs] e definimos esta última como a classe principal do projeto [ui]:
![]() |
- em [1]: cópia de [Dialogue.cs]
- em [2]: colagem
- em [3]: a cópia de [Dialogue.cs]
- em [4]: renomeado para [Dialogue2.cs]
![]() |
- em [6]: define-se [Dialogue2.cs] como a classe principal do projeto [ui].
O código a seguir de [Dialogue.cs]:
// são criadas as camadas [metier et dao]
IImpotMetier metier = null;
try {
// criação da camada [metier]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (ImpotException e) {
// exibição de erro
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// interrupção do programa
Environment.Exit(1);
}
// loop infinito
while (true) {
...
passa a ser o seguinte em [Dialogue2.cs]:
// criação das camadas [metier et dao]
IApplicationContext ctx = null;
try {
// contexto Spring
ctx = ContextRegistry.GetContext();
} catch (Exception e1) {
// exibição de erro
Console.WriteLine("Chaîne des exceptions : \n{0}", "".PadLeft(40, '-'));
Exception e = e1;
while (e != null) {
Console.WriteLine("{0}: {1}", e.GetType().FullName, e.Message);
Console.WriteLine("".PadLeft(40, '-'));
e = e.InnerException;
}
// encerramento do programa
Environment.Exit(1);
}
// solicita-se uma referência na camada [metier]
IImpotMetier metier = (IImpotMetier)ctx.GetObject("metier");
// loop infinito
while (true) {
....................................
- linha 2: IApplicationContext dá acesso a todos os objetos instanciados pelo Spring. Esse objeto é chamado de contexto Spring da aplicação ou, mais simplesmente, de contexto da aplicação. Por enquanto, esse contexto ainda não foi inicializado. É o bloco try/catch a seguir que realiza essa inicialização.
- linha 5: a configuração do Spring em [App.config] é lida e processada. Após essa operação, se não tiver ocorrido nenhuma exceção, todos os objetos da seção <objects> foram instanciados:
- o objeto Spring “dao” é uma instância na camada [dao]
- o objeto Spring “metier” é uma instância na camada [metier]
- linha 19: a classe [Dialogue2.cs] precisa de uma referência na camada [metier]. Essa referência é solicitada ao contexto da aplicação. O objeto IApplicationContext fornece acesso aos objetos Spring por meio de seus nomes (atributo name da tag <object> da configuração Spring). A referência retornada é uma referência ao tipo genérico Object. É necessário converter a referência retornada para o tipo correto, neste caso, o tipo da interface da camada [metier]: IImpotMetier.
Se tudo correu bem, após a linha 19, [Dialogue2.cs] possui uma referência à camada [metier]. O código das linhas 21 e seguintes é o da classe [Dialogue.cs], já estudada.
- linhas 6-17: tratamento da exceção que ocorre quando a análise do arquivo de configuração do Spring não pode ser concluída. Isso pode ocorrer por diversos motivos: sintaxe incorreta do próprio arquivo de configuração ou impossibilidade de instanciar um dos objetos configurados. Em nosso exemplo, esse último caso ocorreria se o arquivo DataImpot.txt da linha 17 do [App.config] não fosse encontrado na pasta de execução do projeto.
A exceção que é reportada na linha 6 faz parte de uma cadeia de exceções, na qual cada exceção possui duas propriedades:
- Mensagem: a mensagem de erro relacionada à exceção
- InnerException: a exceção anterior na cadeia de exceções
O loop das linhas 10 a 14 exibe todas as exceções da cadeia no formato: classe da exceção e mensagem associada.
Ao executar o projeto [ui] com um arquivo de configuração válido, obtêm-se os resultados habituais:
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros
Ao executar o projeto [ui] com um arquivo [DataImpotInexistant.txt] inexistente,
<object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
<constructor-arg index="0" value="DataImpotInexistant.txt"/>
</object>
obtêm-se os seguintes resultados:
- linha 17: a exceção original do tipo [FileNotFoundException]
- linha 15: a camada [dao] encapsula essa exceção em um tipo [Entites.ImpotException]
- linha 9: a exceção lançada pelo Spring porque ele não conseguiu instanciar o objeto chamado “dao”. No processo de criação desse objeto, duas outras exceções ocorreram anteriormente: as das linhas 11 e 13.
- Como o objeto “dao” não pôde ser criado, o contexto da aplicação também não pôde ser criado. Esse é o significado da exceção da linha 5. Antes disso, outra exceção, a da linha 7, havia ocorrido.
- linha 3: a exceção de nível mais alto, a última da cadeia: é sinalizado um erro de configuração.
De tudo isso, devemos reter que é a exceção mais profunda, neste caso a da linha 17, que costuma ser a mais significativa. Note-se, no entanto, que o Spring manteve a mensagem de erro da linha 17 para transmiti-la à exceção de nível superior, na linha 3, a fim de identificar a causa original do erro no nível mais alto.
O Spring, por si só, merece um livro inteiro. Aqui, apenas abordamos superficialmente o assunto. É possível aprofundá-lo com o documento [spring-net-reference.pdf], que se encontra na pasta de instalação do Spring:
![]() |
Também é possível ler o [http://tahe.developpez.com/dotnet/springioc], um tutorial do Spring apresentado no contexto do VB.NET.






























































