Se eu fosse falar "tudo" de arquitetura de projetos, eu acho que passaria muito tempo tentando explicar uma coisa que, talvez, não ficasse tão claro. Então, vou ser um pouco mais prático.

Tentarei ser o mais genérico possível. No entando, nos exemplos irei utilizar C# 2.0 e acesso a dados através do ADO.NET. Mas pode ter certeza que independente da tecnologia que você escolher, a ideia deste artigo irá se aplicar, seja com LINQ, DAAB, NHIBERNATE, MVC ou qualquer outra tecnologia.

Então vamos ao que interessa.

Introdução

Antes de começar, quero dizer que a arquitetura de um sistema vária de projeto para projeto, de necessidade para necessidade. Mas em sua maioria para projetos de médio e grande porte, utilizo as boas práticas de projetos de n-camadas (n-tier). Já para projetos pequenos, utilizo uma arquitetura minimizada (a qual não irei abordar aqui).

É importante ter em mente que a organização demanda tempo. É bem mais rápido desenvolver um projeto de maneira desorganizada do que de maneira organizada. Mas as desvantagens de criar algo não pensado são inúmeras, como: dificuldade de manutenção, difícil refatoração, código não expressivo, entre outras. Então é bom pensar em tudo que for fazer, pensar em cada classe, método, propriedade, que for criar.

Parece ser besteira, mas é sempre bom tomar cuidado com nomes e padrões que serão utilizados no projeto. Mas, sem maiores delongas: "Zelar pelo código que se faz". Em meus projetos costumo utilizar a seguinte arquitetura em camadas:

Zelar pelo código que se faz

Parece ser bem simples não é? E é mesmo. Se tivermos cuidado na hora de arquitetar o sistema e se pensarmos em todos (maioria) dos detalhes do projeto como um todo, iremos ter um sistema fácil de dar manutenção, legível e modular.

Alguns detalhes da arquitetura:

  • Devemos entender que as camadas de baixo referenciam as de cima. NUNCA devemos fazer o processo inverso. A única camada que não referência ninguém e todos conseguem enxerga-lá são as entidades.
  • Existem objetos próprios de cada camada, por exemplo, não iremos ter objeto da Namespace System.Data na camada da UI (Interface com o usuário).
  • Devemos lembrar que objetos que facilitam nossa vida como o SqlDataSource não estão de acordo com padrões de projetos de muitas empresas. Pelo simples fato que ao utilizá-lo estamos indo em contra mão ao ponto que diz (que uma camada se comunica, apenas, com a imediatamente acima). Ao adotarmos o SqlDataSource como controle para manipular dados em nossas aplicações, estamos fazendo com que a GUI acesse diretamente o banco de dados, e na prática apenas a DAL pode acessar nossos meios de persistência a dados.
  • As camadas podem estar distribuídas em máquinas/pastas diferentes, deste que cada camada seja uma DLL. Em outros casos as camadas podem estar dentro de nossas aplicações ASP.NET, sendo pastas na mesma.
  • Os dados devem ser passados para as classes de cima sempre como entidade, ao invés de passar n parâmetros para as camadas de cima, criando assim uma dependência entre camadas. Caso eu adicione um atributo a mais em uma entidade, vou ter que adicionar um parâmetro a mais em meu método. Por exemplo: Não utilize esse tipo de método em seu código:

public void InserirPessoaJuridica(string nome, string CPF, int idade); 

Bem mais simples passar apenas um objeto do que passar 9271823182 de parâmetros.

public void InserirPessoaJuridica
(PessoaJuridica pessoaJuridica);
Nota: O problema de se colocar todas as camadas em um único projeto é se um dia desejarmos criar aquela mesma aplicação para outro tipo de dispositivo. Por exemplo: Temos uma aplicação ASP.NET e desejamos agora criar uma opção para o usuário acessá-la via dispositivo móvel. Caso as camadas estejam em um único projeto, será bem trabalhoso de fazer isto. Já no caso de cada camada ser um projeto diferente, teremos apenas, que criar uma nova camada de “UI” e fazer com que esta referencie as outras camadas.

Outros detalhes serão discutidos no decorrer deste artigo. Agora vamos falar sobre cada camada em particular.

GUI

É a camada de interface com o usuário. No nosso caso é nossa aplicação ASP.NET. Poderia ser um projeto para dispositivo móvel, Windows Form, WPF, Silverlight.

O ideal é que nessa camada encontremos apenas código que estejam diretamente relacionados com nossas páginas aspx.

Já vi desenvolvedores de empresas grandes que faziam coisas esdrúxulas como acessar o bando direto da GUI. Além de deixar o código seboso, o trabalho de refazer o mesmo código inúmeras vezes será enorme.

Então vamos seguir a regra, cada camada só enxerga a imediatamente acima.

Entidade

É a única camada que todos enxergam. Esta entidade contém nossas classes básicas (Ex: Pessoa, Médico, Casa, Cachorro, PessoaJuridica, PessoaFisica...). Classes que representam coisas do nosso mundo real.

Façade

É um padrão faz com que tenhamos todas as funções do sistema em uma única classe. Uma possível implementação para este padrão é utilizar a palavra chave “partial” para ter vários arquivos físicos, mas que no final das contas será uma única classe. A finalidade disto é evitar ter um único arquivo enorme.

OBS: Esta camada é optativa. Você pode acessar diretamente a camada de negócio. Costumo utilizar este padrão em casos onde preciso executar chamadas consecutivas ao banco em uma única operação. Por exemplo, no caso de uma transferência de conta bancária. Preciso retirar de uma pessoa e colocar na conta de outra. Então a façade serviria para fazer a chamada ao devidas funções.

Teria o método Transferência na classe Façade, que este por sua vez iria ter o seguinte trecho de código: PessoaBus.Depositar(x); PessoaBus.Sacar(x);

Ou seja utilizo essa classe apenas quando dou preferência a ter um local de fácil acesso que faça as operações na ordem de execução.

Business Tier

Essa camada contém tudo que for lógica de negócio. Ela que irá fazer verificações e validações dos dados vindos e que ainda vão para o banco.

É importante saber a diferencia entre regras de negócio e validações básicas. Pois existem validações que não precisam ser feitas necessariamente nesta classe. Como por exemplo, se o CPF é válido, pois essa é uma regra geral, e não de um específico sistema. A validação do CPF pode ser feita tanto do lado do cliente (javascript) como na camada de entidades.

DAL

Única camada que acessa a dados. Esta camada é especifica para isso e nada mais. Então é uma boa prática evitar colocar validações nesta classe ou qualquer trecho de código que não esteja diretamente relacionado com acesso a dados.

Em muitos projetos tento criar camadas de acessos a dados genéricos. Mas isso não é obrigatório (dependendo do projeto, lógico). Se você sabe que aquele projeto sempre vai acessar apenas a um banco de dados (Sql Server, Oracle, Mysql, Postgres ou qualquer outro) e não tem a menor chance de mudar, então não existe a necessidade de criar uma camada genérica.

Nota: Criar os repositórios para cada entidade e mapear cada tabela do banco em uma entidade dá muito trabalho. Então existem várias ferramentas que fazem esse trabalho para nós. São as ferramentas de mapeamento objeto relacionais. Temos em .NET como exemplo: Nhibernate, SubSonic, LINQ. Temos um projeto da Microsoft que abstrai o acesso a banco de dados de nossa aplicação. É um projeto grande, que nos fornece muitas utilidades, que é a Microsoft Data Access Application Block. Caso você opte por utilizar alguma dessas ferramentas tudo bem. Mas muito cuidado ao escolher alguma delas. Pois alguns códigos gerados podem ir contra os padrões adotados em seu projeto, o que pode dar muita dor de cabeça ao tentar modificar o código gerado pela mesma.

Quanto a conceitos de camadas, o que posso dizer é isso. Aconselho a estudar coisas relacionadas a “design partner”. Com bom conhecimento de padrões de projetos e bom conhecimento da regra de negócio da aplicação você será capaz de escrever aplicações bem arquitetadas e modeladas.

Codificando

Como exemplo, vou mostrar um cadastro de pessoa bem simples. Apenas para dar ideia de como seria um sistema em camadas que costumo utilizar em alguns projetos pessoais. Porém, longe de ser o modelo ideal para suas aplicações. Para chegar a um modelo “ideal” para A aplicação procuro me sentar com toda a equipe de desenvolvimento (se não apenas os mais experientes) para discutirmos o modelo de nossa aplicação e de algumas classes.

Para este exemplo, irei fazer uma aplicação ASP.NET e C# 2.0, acessando os dados com ADO.NET. Fazendo tudo isto na mão, sem o auxílio de ferramenta alguma.

Bem, em meus projetos costumo começar de cima para baixo, ou seja, crio as camadas na seguinte sequência: Entidade > DAL > Business > Façade > GUI. Pois me faz pensar primeiramente no problema como um todo, e me faz refletir sobre as possíveis operações e necessidade do sistema.

Modelando o banco

O banco será bem simples, terá apenas a tabela pessoa.

Banco

O script para a tabela é:


CREATE TABLE [dbo].[Pessoa](
[id] [int] IDENTITY(1,1) NOT NULL,
[nome] [nvarchar](100) NULL,
[dataNascimento] [smalldatetime] NULL,
[sexo] [nchar](1) NULL,
[email] [nvarchar](50) NULL,
[cpf] [nvarchar](15) NULL,
CONSTRAINT [PK_Pessoa] PRIMARY KEY CLUSTERED
)

Entidade

A camada de entidade é básica de se fazer, é basicamente um mapeamento das tabelas do banco. Para cada tabela do banco, teremos uma classe na camada de entidades.


public class Pessoa     
{      
   #region Atributos      

   private DateTime _dateNascimento;      

   #endregion      

   #region Construtores      

   public Pessoa()      
   {      

   }      

   public Pessoa(int id)      
   {      
       this.ID = id;      
   }      

   public Pessoa(string nome, DateTime dataNascimento, 
          char sexo, string email, string cpf)      
   {      
       this.Nome = nome;      
       this.DataNascimento = dataNascimento;      
       this.Sexo = sexo;      
       this.Email = email;      
       this.Cpf = cpf;      
   }      

   public Pessoa(int id, string nome, DateTime dataNascimento, 
          char sexo, string email, string cpf)      
       : this(nome, dataNascimento, sexo, email, cpf)      
   {      
       this.ID = id;      
   }      

   #endregion      

   #region Propriedades      

   public int ID { get; set; }      

   public string Nome { get; set; }      

   public DateTime DataNascimento      
   {      
       get      
       {      
           return this._dateNascimento;      
       }      
       set      
       {      
           if (value > DateTime.Now)      
           {      
               throw new DataInvalidaException();      
           }      
           else      
           {      
               this._dateNascimento = value;      
           }      
       }      
   }      

   public char Sexo { get; set; }      

   public string Email { get; set; }      

   public string Cpf { get; set; }      

   #endregion      
}

Dá para notar que na propriedade DataNascimento é feita uma validação. Essa validação pode ser feita nesta camada, pelo fato de ser uma validação básica, onde não muda de sistema para sistema. Ou seja, em todo local do mundo a data de nascimento de uma pessoa vai ser menos que a data atual.

Camada de acesso a dados

Para criar as camadas de acesso a dados, costumo criar uma (ou mais) interfaces ou classes abstratas que servirão como base criar as classes de acesso a dados de cada entidade. Esta classe ou interface contém definições de métodos básicos, que sei que todas as classes vão conter, como por exemplo: Inserir, Atualizar, Recuperar por ID, Deletar.

Para o exemplo, irei utilizar a seguinte interface:


/// 
/// Interface com todos os métodos necessários para uma classe de DAO
/// 
/// tipo do objeto que será manipulador pela DAO
public interface IDataAccessObject where T : new()
{
T Get(K id);

void Insert(T obj);

void Update(K id, T obj);

void Delete(K id);
}

Esta é uma interface genérica que contém métodos básicos para outras classes. Alguns métodos são genéricos por recebem o tipo da chave primária da entidade (int, long, short).

Outra classe da DAL que costumo utilizar é uma classe para auxiliar coisas como: criação de parâmetros, criação de comandos, entre outras operações. Como falei anteriormente, você pode utilizar a Microsoft Data Access Application Block para ajudar com esse tipo de coisas. Essa biblioteca contém inúmeros métodos.

Para a aplicação de demonstração, irei utilizar uma classe bem simples que fiz apenas para esse exemplo. Detalhe que não estou abstraindo o banco da aplicação, mas aconselho você a criar uma classe que abstraia o banco que esta se utilizando. Outra observação é que está classe está incompleta, fiz apenas para exemplo mesmo. Esta é uma classe que tem que ser bem pensada e bem trabalhada com a finalidade de obter melhor desempenho do banco de dados que está usando. Garanto que não será difícil modificar esta classe para ser genérica. Segue o código da mesma:


public class DatabaseHelper     
{      
   #region Propriedades      

   public SqlConnection MyBdConnection { get; set; }      
   public string NomeStringConexao { get; set; }      

   #endregion      

   #region Construtores      

   public DatabaseHelper()      
   {      
       this.NomeStringConexao = "DefaultStringConexao";      
       this.MyBdConnection = new SqlConnection(this.NomeStringConexao);      
   }      

   public DatabaseHelper(string nomeStringConexao)      
   {      

       this.NomeStringConexao = nomeStringConexao;      
       this.MyBdConnection = new SqlConnection(this.NomeStringConexao);      
   }      

   #endregion      

   #region Métodos Privados      

   private string GetCorrectParameterName(string parameterName)      
   {      
       if (parameterName[0] != '@')      
       {      
           parameterName = "@" + parameterName;      
       }      
       return parameterName;      
   }      

   #endregion      

   #region Métodos Públicos      

   public static DatabaseHelper Create()      
   {      
       return new DatabaseHelper();      
   }      

   public static DatabaseHelper Create(string nomeStringConexao)      
   {      
       return new DatabaseHelper(nomeStringConexao);      
   }      

   public void OpenConnection()      
   {      
       if (this.MyBdConnection.State == System.Data.ConnectionState.Closed)      
       {      
           this.MyBdConnection.Open();      
       }      
   }      

   public void CloseConection()      
   {      
       this.MyBdConnection.Close();      
   }      

   public SqlParameter BuildParameter(string nome, object valor, 
         DbType tipo, int size)      
   {      
       SqlParameter parametro = new SqlParameter
             (this.GetCorrectParameterName(nome), valor);      
       parametro.DbType = tipo;      
       parametro.Size = size;      
       return parametro;      
   }      

   public void BuildParameter(string nome, object valor, 
         DbType tipo, int size, List listParametros) 
   {      
       SqlParameter parametro = this.BuildParameter(nome, valor, tipo, size);      
       listParametros.Add(parametro);      
   }      

   public SqlParameter BuildOutPutParameter(string nome, DbType tipo, int size)      
   {      
       SqlParameter parametro = new SqlParameter();      
       parametro.ParameterName = this.GetCorrectParameterName(nome);      
       parametro.DbType = tipo;      
       parametro.Size = size;      
       parametro.Direction = ParameterDirection.Output;      
       return parametro;      
   }      

   public void BuildOutPutParameter(string nome, DbType tipo, 
          int size, List listParametros)      
   {      
       SqlParameter parametro = this.BuildOutPutParameter(nome, tipo, size);      
       listParametros.Add(parametro);      
   }      

   public void ExecuteNonQuery(SqlCommand command)      
   {      
       command.ExecuteNonQuery();      
   }      

   public void ExecuteNonQuery(SqlCommand command, bool openConnection)      
   {      
       if (openConnection)      
       {      
           this.OpenConnection();      
       }      
       this.ExecuteNonQuery(command);      
       if (openConnection)      
       {      
           this.CloseConection();      
       }      
   }      

   public void ExecuteNonQuery(string query, params SqlParameter[] parameters)      
   {      
       Exception erro = null;      
       try      
       {      
           this.OpenConnection();      
           SqlCommand command = this.MyBdConnection.CreateCommand();      
           command.CommandText = query;      
           command.Parameters.AddRange(parameters);      
           this.ExecuteNonQuery(command);      
           this.CloseConection();      
       }      
       catch (Exception ex)      
       {      
           erro = ex;      
       }      
       finally      
       {      
           this.CloseConection();      
       }      

       if (erro != null)      
       {      
           throw erro;      
       }      
   }      

   public void ExecuteCommands(params SqlCommand[] commands)      
   {      
       Exception erro = null;      
       SqlTransaction trans = null;      
       try      
       {      
           this.MyBdConnection.Open();      
           trans = this.MyBdConnection.BeginTransaction();      
           for (int i = 0; i < commands.Length; i++)      
           {      
               commands[i].Transaction = trans;      
               this.ExecuteNonQuery(commands[i]);      
           }      
           trans.Commit();      
           this.MyBdConnection.Close();      
       }      
       catch(Exception ex)      
       {      
           trans.Rollback();      
           erro = ex;      
       }      
       finally      
       {      
           this.MyBdConnection.Close();      
       }      

       if (erro != null)      
       {      
           throw erro;      
       }      
   }      

   #endregion      
}

Para criar essa classe auxiliar deve se considerar muitas coisas:

  • Abrangência;
  • Desempenho;
  • Utilização de cachê ou não;
  • Objetos que serão utilizados para manipular os dados (DataDet, DataTable, SqlDataReader);
  • Se será utilizado apenas stored procedure ou se todas as querys vão estar na aplicação.

Já temos uma interface para nossa camada de acesso a dados e uma classe que irá nos auxiliar com a mesma. Agora nos resta criar as classe de acesso para cada entidade presente em nosso projeto. O código da nossa classe Pessoa será o seguinte:


public class PessoaDAO : IDataAccessObject     
{      
   #region Atributos      

   private DatabaseHelper databaseHelper;      

   #endregion      

   #region Construtores      

   public PessoaDAO()      
   {      
       databaseHelper = DatabaseHelper.Create();      
   }      

   #endregion      

   #region IDataAccessObject Members      

   public Pessoa Get(K id)      
   {      
       Pessoa pessoa = new Pessoa();      
       SqlDataReader reader = null;      
       try      
       {      
           string query = "SELECT * FROM Pessoa WHERE id = @id";      
           databaseHelper.OpenConnection();      
           reader = databaseHelper.ExecuteDataReader(query,      
               new SqlParameter("id", id));      
           while (reader.Read())      
           {      
               pessoa.Nome = reader["nome"].ToString();      
               pessoa.Cpf = reader["cpf"].ToString();      
               pessoa.DataNascimento = Convert.ToDateTime(reader["dataNascimento"]);      
               pessoa.Email = reader["email"].ToString();      
               pessoa.ID = Convert.ToInt32(reader["nome"]);      
           }      
           reader.Close();      
           this.databaseHelper.CloseConection();      
       }      
       finally      
       {      
           if (reader != null)      
           {      
               reader.Close();      
           }      
           this.databaseHelper.CloseConection();      
       }      
       return pessoa;      
   }       

   public void Insert(Pessoa obj)      
   {      
       string query = "INSERT INTO PESSOA (nome, dataNacimento, cpf, email, sexo) 
            VALUES (@nome, @dataNascimento, @cpf, @email, @sexo)"; 
       this.databaseHelper.ExecuteNonQuery(query,      
           new SqlParameter("nome", obj.Nome),      
           new SqlParameter("dataNascimento", obj.DataNascimento),      
           new SqlParameter("cpf", obj.Cpf),      
           new SqlParameter("email", obj.Email),      
           new SqlParameter("sexo", obj.Sexo));      
   }      

   public void Update(K id, Pessoa obj)      
   {      
       throw new NotImplementedException();      
   }      

   public void Delete(K id)      
   {      
       throw new NotImplementedException();      
   }      

   #endregion      
}

Simples não é?! A implementação dos métodos de delete e update, eu deixo como exercício para você.

Camada de Negócio

A ideia da camada de negócio é bastante simples. Como falei anteriormente, ela tem o papel de acessar a camada de dados e é quem fará validações em cima da mesma. Geralmente, assim como a camada de DAL, costumo criar uma interface nessa camada com os métodos mais utilizados. Mas para exemplo irei mostrar apenas o método insert da classe de negócio de pessoa.


public class PessoaBus     
{      
   private PessoaDAO pessoaDAO;      

   public PessoaBus()      
   {      
       this.pessoaDAO = new PessoaDAO();      
   }      

   public void Inserir(Pessoa pessoa)      
   {      
       //caso haja validação, ela pode ser feita neste método mesmo.      
       this.pessoaDAO.Insert(pessoa);      
   }      
}

Um padrão que costumo utilizar nessa camada são as factories. Que tem o papel de criar instâncias das classes. Ou seja, com esse padrão eu consigo ter acesso a qualquer objeto acessando apenas uma classe. Exemplo de Factory:


public static class FactoryDAO     
{      
   public static ContatoDAO CreateContatoDAO()      
   {      
       return new ContatoDAO();      
   }      

   public static ContatoDAO CreatePessoaDAO()      
   {      
       return new PessoaDAO();      
   }      
}

Façade Tier

Façade é simples o bastante que dispensa palavras. Então vamos ao exemplo:


public partial class Facade     
{      
   private PessoaDAO _pessoaDAO;      
   private ContatoDAO _contatoDAO;      

   public Facade()      
   {      
       _contatoDAO = FactoryDAO.CreateContatoDAO();      
       _pessoaDAO = FactoryDAO.CreatePessoaDAO();      
   }      
}
Listagem 1. Facade.cs

public partial class Facade     
{      
   public void InserirPessoa(Pessoa pessoa)      
   {      
       this._pessoaDAO.Insert(pessoa);      
   }      

   public void UpdatePessoa(int id, Pessoa pessoa)      
   {      
       this._pessoaDAO.Update(id, pessoa);      
   }      

   public Pessoa GetPessoa(int id)      
   {      
       return this._pessoaDAO.Get(id);      
   }      

   public void DeletePessoa(int id)      
   {      
       this._pessoaDAO.Delete(id);      
   }      
}
Listagem 2. PessoaFacade.cs
Nota: Notar o uso da palavra chave “partial”.

UI Tier

Não irei mostrar exemplo de código desta camada. Pois este é o nosso projeto ASP.NET que já conhecemos. A única coisa que devemos fazer nessa camada é encapsular os dados no objeto Pessoa, em algum evento, e em seguida passar o mesmo para algum método da Façade.

Bem, espero ter ajudado a esclarecer como funciona uma aplicação em c# bem arquitetada.

O básico de uma boa arquitetura de sistema é isto. Pode ter certeza que lendo esse material, depois de ter entendido o mesmo por completo e depois ler profundamente sobre padrões de projetos você estará totalmente apto para criar uma aplicação bem modelada e arquitetada.