Os padrões de projeto na engenharia de software se referem a organização de componentes para resolver um problema específico. Essa estrutura fornece uma terminologia comum para os desenvolvedores e como implementar esses padrões quando o problema surgir.

A história dos padrões surgiu com o conhecido grupo chamado de “Gang of Four” (GoF), formado por quatro profissionais da mais alta qualidade e com experiência em diversos projetos. Deles saiu o famoso livro “Design Patterns: Elements of Reusable Object-Oriented Software”, que contém diversos padrões de projetos. Atualmente diversos outros livros de padrões surgiram para as mais variadas linguagens de programação e plataformas de software.

Os padrões arquiteturais surgiram ainda antes dos padrões de projeto e são mais utilizados atualmente devido o surgimento de novas plataformas como mobile e web.

No restante do artigo serão analisados os principais padrões de projetos e arquiteturais. Por fim, também veremos como as camadas de software podem ser distribuídas através de plataformas de hardware.

Padrão de Projeto Observer

O Observer é um padrão comportamental também conhecido como “publish-subscribe”, que consiste basicamente de dois componentes: o Subject e o Observer.

A entidade conhecida como Subject exibe eventos que as outras entidades, conhecidas como Observers, podem se registrar para escutar ou receber notificações quando os eventos acontecem. Os Observers podem se “desregistrar” quando eles não possuem mais interesse em ser notificados de novos eventos.

Na maioria das implementações o Observer implementa uma interface que é especificada pelo Subject.

Segue na Figura 1 um diagrama que mostra os componentes básicos do padrão.

Diagrama estrutural do padrão Observer

Figura 1. Diagrama estrutural do padrão Observer.

O código da Listagem 1 mostra a implementação típica de um método do Subject para enviar notificações aos Observers registrados.

Listagem 1. Método que envia notificações para os Observers registrados no Subject.


  NotificaAlteracoes() {
           foreach(observerlist in IObserversList) {
                     observerlist.ProcessaAlteracao()
           }
  }

O método possui um loop que percorre a lista de Observers chamando a interface de cada método.

Opcionalmente, se o Subject precisar fornecer informações para o(s) Observer(s) ele também pode implementar uma interface para que o(s) Observer(s) possa(m) usar essa interface para se comunicar com o Subject. A interface deste último normalmente expõe informações internas adicionais para que, após uma notificação, um Observer possa chamar de volta o Subject para informações adicionais.

Entre as aplicações que costumam utilizar este padrão incluem Chats, atualizações de informações como os dados climáticos, entre outros.

Padrão de Projeto Facade (Estrutural)

A proposta deste padrão é simplificar uma interface externa para um sistema interno que possui relativa complexidade na sua interação. Isto permite que um sistema interno seja modularizado e dividido em componentes independentes, oferecendo assim uma melhor performance e um teste mais simplificado. Um cliente do sistema usando o Facade tem uma interface mais simples, como podemos ver na estrutura da Figura 2.

Diagrama estrutural do padrão Facade

Figura 2. Diagrama estrutural do padrão Facade.

Na figura verifica-se que o cliente possui apenas os métodos "Method_1()" e "Method_2()" e o restante do sistema não é reconhecido pelo cliente. Cada chamada pode fazer uso de um ou mais dos subsistemas definidos.

Um exemplo seria o Method_1() fazer uso de dois métodos de SubSys1 e o resultado disso seria utilizado como entrada em SubSys2. Essa complexidade seria totalmente ofuscada por Method_1(), que é a única chamada disponível para o cliente, desprezando assim tudo que está por trás dessa complexidade.

Padrão de Projeto Singleton

O Singleton garante que existe apenas uma única instância de uma classe criada. Nas linguagens como Java e C# esse padrão é construído através de uma combinação de um construtor privado e métodos estáticos como Create() para C# e getInstance() para Java. Assim, esses métodos estáticos retornam uma referência de uma instância já existente, de forma que todos os clientes compartilham essa mesma instância.

Esse padrão é muito utilizado em diversas aplicações principalmente quando é bastante custoso ficar criando e destruindo classes que possuem recursos pesados.

Padrão de Projeto Factory Method

O padrão Factory Method é utilizado para definir a criação de objetos. Ele é bastante útil quando uma de muitas subclasses são necessárias, mas não se sabe necessariamente qual será executada em tempo de execução.

Um exemplo clássico deste padrão é a classe FrameworkApplication, que usa uma classe FrameworkWindow de um sistema gráfico. Ela define um método para criação de objetos FrameworkWindow.

Uma variação comum deste padrão é o Parameterized Factory Method, onde um método aceita um parâmetro e retorna um objeto baseando-se neste parâmetro. O tipo retornado pelo método é uma interface ou classe base. O objeto retornado implementa a interface ou é uma classe derivada.

O benefício do padrão Factory Method e da sua derivação é que ele centraliza a criação de objetos para uma localização conhecida e também permite que o cliente do método trabalhe com uma definição comum que pode ser expandido caso necessário.

Um exemplo prático deste padrão será mostrado mais a frente, juntamente com o padrão Strategy, que será discutido na próxima seção.

Padrão de Projeto Strategy

O padrão é utilizado para encapsular um determinado comportamento em uma interface padrão.

As classes que implementam esta interface podem utilizar diferentes algoritmos ou métodos privados para realizar as suas tarefas, no entanto, o comportamento externo da classe deve permanecer o mesmo. Com isso, pode-se ter variações nos detalhes da implementação atual enquanto a interface pública é a mesma. Apesar do padrão ser escrito utilizando interfaces, também é possível utilizar uma classe base. Normalmente, se existe algum comportamento padrão desejado, utiliza-se uma classe base, caso contrário é utilizada uma interface.

Um exemplo clássico de implementação do Strategy é a interface ISortable, que possui um método único chamado Sort(). Dessa forma, essa interface possui implementações com diferentes comportamentos dependendo do algoritmo que a implementa. Por exemplo, tem-se as classes BubbleSort, QSort, BucketSort, entre outras que implementam a interface padrão ISortable e o método Sort(). Assim, qualquer código que a utiliza não conhece os detalhes da implementação, apenas chama ISortable.Sort() e o conjunto de elementos desejado é corretamente ordenado.

Utilizando os padrões Factory Mehtod e Strategy para Refatoração

Os padrões Factory Method e Strategy são muito utilizados para refatorações.

O código da Listagem 2 é uma aplicação que imprime a informação em um formato de relatório com seus headers e a descrição nas respectivas linhas.

Para esse exemplo primeiramente considera-se que a classe InfosRelatorio representa alguma informação básica. Embora o código a seguir execute, tem-se diversos problemas como: muita manutenção para alterar o código, que é muito extenso, alto acoplamento, baixa coesão, entre outros problemas.

Listagem 2. Relatório com diversos problemas no código exigindo uma refatoração posterior.


  class Program
  {
  private const string RELATORIO_NOME = "TESTE DE RELATÓRIO";
  private const string COL_1 = "ID";
  private const string COL_2 = "Coluna 2 Exemp";
  private const string COL_3 = "Coluna 3 Exemp";
   
  static void Main(string[] args)
  {
  Console.WriteLine(RELATORIO_NOME);
  Console.WriteLine("{0,5} {1,12} {2,12}",
  COL_1, COL_2, COL_3);
   
  List<InfosRelatorio> relatorioInfo = GetRelatorioInfo();
   
  foreach (InfosRelatorio item in relatorioInfo)
  {
  Console.WriteLine("{0,5} {1,12} {2,12}",
  item.ID, item.InfoColDois,
  item.InfoColTres);
  }
   
  Console.Write("\n\nPressione Enter para Sair...");
  Console.ReadLine();
  }
   
  private static List<InfosRelatorio> GetRelatorioInfo()
  {
  List<InfosRelatorio> list1 = new List<InfosRelatorio>()
  {
   
  new InfosRelatorio(){ID=1,InfoColDois="Higor",
  InfoColTres=22},
  new InfosRelatorio(){ID=2,InfoColDois="Daniel",
  InfoColTres=37},
  new InfosRelatorio(){ID=3,InfoColDois="Evandro",
  InfoColTres=43}
  };
   
  return list1;
  }
  }

Como um exemplo, pode-se imaginar a situação em que seja necessário adicionar novos relatórios. Nesse caso, deveriam ser criadas constantes adicionais para as novas colunas? Como seriam determinadas as colunas apropriadas? E se cada relatório, teoricamente, precisasse de diferentes fontes de dados, como disponibilizá-las nesse código?

Assim, a Listagem 2 precisa ser refatorada e para isso deve-se utilizar os padrões de projeto, tornando a aplicação mais orientada a objetos.

A primeira providência é criar uma classe ReportFactory que utiliza o padrão Parameterized Factory Method para decidir qual relatório criar e retornar. Será retornado um objeto que implementa a interface IReportStrategy que representa um relatório.

Agora o Main() está conforme a Listagem 3.

Listagem 3. Método Main refatorado.


  class Program
  {
  static void Main(string[] args)
  {
  Console.Write("Digite o nome do Relatório: ");
  String nomeRelatorio = Console.ReadLine();
  IReportStrategy rs = ReportFactory.GetRelatorio(nomeRelatorio);
   
  if (rs != null)
  {
  Console.WriteLine(rs.GetColunas());
  for (int i = 0; i < rs.GetNumLinha(); i++)
  Console.WriteLine(rs.GetData(i));
  }
  else
  Console.WriteLine("Relatório {0} não encontrado.",
  nomeRelatorio);
   
  Console.Write("\n\nPressione Enter para Sair...");
  Console.ReadLine();
  }
  }

Pode-se verificar que esse código é muito mais limpo que o anterior. Também tem os detalhes do relatório separado da implementação.

O interessante é que agora esta função pode suportar qualquer número de relatórios adicionais apenas modificando a factory e o strategy. Assim, segue nas Listagens 4 e 5 o IReportStategy e o ReportFactory.

Listagem 4. Interface do Strategy.


  interface IReportStrategy
  {
  String GetNomeRelatorio();
  String GetColunas();
  int GetNumLinha();
  String GetData(int linha);
  }

Listagem 5. Implementação do Factory.


  class ReportFactory
  {
  public static IReportStrategy GetRelatorio(String nomeRelatorio)
  {
  IReportStrategy strategy = null;
  if (nomeRelatorio.Equals("Relatorio1"))
  strategy = new Relatorio1();
  else if (nomeRelatorio.Equals("Relatorio2"))
  strategy = new Relatorio2();
   
  return strategy;
  }
  }

Agora cada relatório sabe da sua própria coluna e pode carregar a sua própria informação. Nos exemplos apresentados tem-se dois relatórios: Relatorio1 e Relatorio2. Será implementado na Listagem 6 o algoritmo do Relatorio1 para implementar o Relatorio2 bastando criar uma nova classe chamada Relatorio2 com a sua respectiva implementação. Com isso, pode-se notar que temos apenas a criação de uma nova classe, com o mínimo de impacto no restante do código. Assim temos a implementação do Strategy.

Listagem 6. Implementação do Strategy para um determinado relatório.


  class Relatorio1 : IReportStrategy
  {
   
  List<InfosRelatorio> _info = new List<InfosRelatorio>();
   
  public Report1() { CarregaInfosIniciais(); }
   
  public string GetNomeRelatorio() { return "TESTE DE RELATÓRIO"; }
   
  public string GetColunas()
  {
  return String.Format("{0,5} {1,12} {2,12}",
  "ID", " Coluna 2 Exemp", " Coluna 3 Exemp");
  }
   
  public int GetNumLinha() { return _info.Count; }
   
  public string GetData(int linha)
  {
  InfosRelatorio rd = _info[linha];
   
  return String.Format("{0,5} {1,12} {2,12}",
  rd.ID, rd.InfoColDois, rd.InfoColTres);
  }
   
  private void CarregaInfosIniciais()
  {
  _info.AddRange(new List<InfosRelatorio>()
  {
   
  new InfosRelatorio(){ID=1, InfoColDois="Higor",
  InfoColTres=22},
  new InfosRelatorio(){ID=2, InfoColDois="Daniel",
  InfoColTres=37},
  new InfosRelatorio(){ID=3, InfoColDois="Evandro",
  InfoColTres=43}
  });
  }
  }
  

Padrão Arquitetural MVC (Model-View-Controller)

O MVC é um dos padrões arquiteturais mais antigos. Este padrão foi originalmente desenvolvido na linguagem Smalltalk sendo extremamente utilizado atualmente, muito em função das aplicações web e mobile.

Segue na Figura 3 a estrutura deste padrão.

Estrutura do padrão Model-View-Controller
(MVC).

Figura 3. Estrutura do padrão Model-View-Controller (MVC).

Este padrão possui três componentes que são melhores definidos a seguir:

  • Model: representa a informação e a lógica de negócio do sistema;
  • View: recebe entradas do sistema e exibe os resultados para os clientes. A View normalmente é algum tipo de GUI, mas não possui necessariamente uma interface gráfica;
  • Controller: aceita entradas da View e requisições de informações do Model. Além disso, determina as ações para a View e para o Model baseado em uma lógica programada.

A grande vantagem deste padrão arquitetural é que a View é separa do Model e assim ela pode ser alterada sem impactar o Model. Um exemplo disso é um sistema complexo com diversas Views: imagine se fosse necessário alterar o Model a cada vez que a View fosse alterada - seria impraticável manter este sistema. Além disso, o Model e o Controller podem ser testados e verificados sem uma View, utilizando assim testes unitários e mocks objects, que são ferramentas facilitadoras para produção de testes automatizados.

Esse padrão é extremamente benéfico nas aplicações distribuídas atuais, como as mobiles e web, que podem ser conectadas ao mesmo Model. O Controller situando-se em frente ao Model pode analisar cada requisição e otimizar a resposta baseando-se no cliente da requisição. Com isso, tem-se uma aplicação bastante flexível e de fácil manutenção.

Algumas observações são importantes referentes ao padrão MVC:

  • O Controller pode suportar muitas Views;
  • A View tem uma dependência no Model, porém este não depende de qualquer outro componente;
  • O Controller se comunica diretamente com a View e com o Model.

Padrão Arquitetural Inversion of Control

O padrão arquitetural Inversão de Controle (Inversion of Control ou IoC) também é conhecido como Injeção de Dependência (Dependency Injection ou DI), e é muito importante para sistemas que possui componentes distribuídos e que devem ser testados separadamente.

Ele funciona da seguinte forma: se uma classe faz uso do serviço de outra classe, o cliente do serviço não deveria criar instâncias deste. Ao invés disso, o provedor deveria definir uma abstração, que consta normalmente de uma interface, que o cliente deveria simplesmente utilizar.

Segue na Figura 4 a estrutura deste padrão.

Estrutura do padrão Inversion of Control

Figura 4. Estrutura do padrão Inversion of Control.

O cliente pode manter uma referência para a interface e usar qualquer classe que implemente esta. Na fase de testes os objetos mocks (aqueles que simulam objetos reais) podem implementar a interface.

Apesar deste termo parecer estranho, o nome “inversion of control” se dá porque o cliente não controla a criação de objetos dependentes, pois eles são criados externamente e são disponibilizados ao cliente. A injeção de dependência é realizada quando a implementação do serviço externo se torna disponível ao cliente. A injeção de dependência pode ser implementada em uma das três formas: injeção no construtor, injeção nos métodos setting ou injeção em algum método.

Padrão Arquitetural N-Tier

Os padrões discutidos anteriormente se referem a arquitetura lógica do software, no entanto, também se faz importante analisar como a arquitetura de hardware apoia o projeto do software.

Alguns autores referenciam este padrão arquitetural como "3-Camadas", porém o mais correto é chamá-lo de "N-Camadas", pois seria um número maior ou igual a três. A arquitetura "3-Camadas" foi o nome originalmente dado como uma evolução da antiga arquitetura “cliente-servidor” também conhecida como arquitetura “2-Camadas”. No entanto, com o passar do tempo, mais camadas foram introduzidas para oferecer maior extensibilidade e resiliência, visto que os sistemas se tornaram ainda mais complexos.

Segue na Figura 5 a estrutura da arquitetura 3-Camadas.

Diagrama estrutural da Arquitetura 3-Camadas

Figura 5. Diagrama estrutural da Arquitetura 3-Camadas.

Essa arquitetura tem similaridade com a arquitetura MVC, visto que cada componente é responsável por uma operação específica. Na arquitetura apresentada cada componente é uma camada lógica representando pelo menos um componente físico e possivelmente mais do que um. Essa distribuição também pode ser afetada por escolhas na comunicação entre as camadas, em que protocolos de rede não introduziriam mais camadas, mas usando uma fila de mensagens ou algum tipo de serviço de entrega garantida entre os componentes produziriam camadas adicionais.

Outra forma comum de adicionar mais camadas é utilizar a tecnologia disponível em outra camada para aplicar segurança e regras de negócio adicionais. Por exemplo, utilizar store procedures na base de dados (camada de dados) é uma forma de checar novamente as regras de negócio introduzidas em outra camada da aplicação, porém isto também fornece uma segurança adicional.

Da mesma forma, dividir a camada lógica de negócio em uma camada de Workflow e uma camada de regra de negócio é algo bastante comum, já que produtos Workflow tem se tornado bastante popular. A camada de Workflow controla qual tela será exibida para o usuário. A camada de negócio checa os dados e impõe que as regras de negócio sejam processadas.

Segue na Figura 6 a estrutura da arquitetura multicamadas com camadas adicionais.

Diagrama estrutural da Arquitetura
Multicamadas

Figura 6. Diagrama estrutural da Arquitetura Multicamadas.

Analisando as duas arquiteturas apresentadas pode-se ter cada camada residindo em um ou diferentes servidores. Isto depende de diversos fatores como a política da empresa, nível de segurança, desempenho, entre outros. Um exemplo são os servidores de aplicação e os servidores web residirem na mesma máquina física.

Segue na Figura 7 um diagrama físico que exemplifica o que foi discutido.

Exemplo de arquitetura física 3-Camadas

Figura 7. Exemplo de arquitetura física 3-Camadas.

Pode-se verificar que o servidor web e as bibliotecas estão localizados na mesma máquina física, assim tem-se a combinação da camada de apresentação e da camada de negócio em um único servidor. Neste caso o firewall está separado e pode ser programado para proibir certas conexões.

A nível de software, uma forma de separar a aplicação utilizando o Microsoft ASP.NET seria utilizar classes compiladas em DLLs assemblies separadas e carregá-las apenas quando necessárias. Em uma aplicação Java Web isto poderia ser realizado através de uma classe Servlet que exibe a página para o usuário e acessa um JAR na camada de negócio, que reside em outro servidor. Vale ressaltar que a camada de dados é apenas acessada através da camada de negócio. Assim, a camada de negócio se comunica diretamente com a camada de apresentação e com a camada de dados.

Outra situação que ocorre em algumas aplicações é a camada de negócio presente na camada de dados. Nesse caso, tem-se que as store procedures do banco de dados contém as regras necessárias da aplicação. No entanto, vale ressaltar que isso não é interessante de ser realizado. Diversas experiências em projetos mostram que as regras de negócio no banco de dados apenas dificultam a manutenção. Além disso, se fosse preciso a troca do banco de dados tudo teria que ser refeito, tornando a migração da aplicação quase impraticável.

A complexidade de uma aplicação e o número de sistemas externos integrados também pode definir uma arquitetura física e lógica diferente para a aplicação. A Figura 8 mostra uma aplicação mais complexa, porém muito comum na indústria de software. Nesse caso, os componentes lógicos são os mesmos da Figura 8.

Exemplo de arquitetura multicamadas mais
complexa

Figura 8. Exemplo de arquitetura multicamadas mais complexa.

Podemos verificar a presença de um servidor de load-balancing (load-balancing server ou LBS), que é necessário quando a aplicação precisa estar disponível o tempo todo, mesmo durante uma manutenção programada. O LBS é utilizado para decidir qual servidor web deve ser acessado. Se um servidor está parado ou ocupado com muito tráfego, este será automaticamente redirecionado para outro servidor. Este tipo de disposição necessidade de um web session data, em que a informação é armazenada na memória para depois ser armazenada na base de dados. Isso é importante para o caso de um usuário possuir múltiplas requisições que usa data session e essas requisições serem gerenciadas por diferentes servidores, assim a session data deve estar disponível. Dessa forma, tem-se que o LBS, os dois servidores web e o session database representam a camada de apresentação.

O servidor de aplicação contém múltiplos componentes lógicos. Tanto a camada de regras de negócio quanto a camada data object estão ambas presentes nesses servidores. Pode-se notar que a camada Workflow também é utilizada pela camada de aplicação para certas decisões operacionais. Os data objects são utilizados para enviar informações para camada de aplicação que serão exibidas para o usuário, além de informações para o Workflow afim de processamento ou avaliação. Além disso, o servidor de aplicação também pode utilizar um servidor de e-mail para notificações. Em algumas arquiteturas este servidor de aplicação separado está contido na camada de negócio.

O Application Database possui dois componentes lógicos: a camada data rules e a data. A camada data rules são as stored procedures, restrições e funções. Essa camada amplia ou duplica as funções da camada de negócio. A camada de dados poderia incluir um servidor de arquivos (file server), visto que a base de dados poderia não armazena as informações localmente, ao invés disso, utilizaria um servidor de arquivo para fornecer espaço em disco separado do servidor de banco de dados. Esse tipo de situação é bastante comum em grandes empresas que necessitam de resiliência e facilidade para adicionar mais espaço.

Após analisar a figura anterior que mostra as camadas físicas, pode-se realizar um mapeamento dessas camadas para as camadas lógicas (Apresentação, Regras de Negócio, Data Object, Camada de Dados e Workflow). Existem diversas formas de atribuir uma camada lógica às camadas apresentadas. Por exemplo, alguns especialistas poderiam argumentar que o servidor de e-mail poderia ser parte da camada de dados, outros diriam que faz parte da camada de negócio. O Web Session Database poderia ser incluído como parte da camada de apresentação ou da camada de dados. Por isso, o mapeamento não precisa estar absolutamente correto, pois não existe um padrão para isso, na verdade a ideia principal é demonstrar diretrizes gerais e garantir que as camadas lógicas têm um mapeamento. Este mapeamento é bastante útil quando é preciso determinar o impacto de um upgrade ou instalação de algum patch, além de garantir que todos os servidores estão sendo utilizados na organização.

Por fim, vale salientar que estes ambientes podem estar duplicados ou até triplicados para serem utilizados como ambientes de teste, desenvolvimento ou recuperação de desastres. Esses fatores dependem do tamanho da organização, política da empresa, entre outros. Os diagramas se tornam ainda mais importantes nesses casos, visto que são necessários para manter tudo organizado e garantir que os diferentes ambientes são rigorosamente os mesmos.

Bibliografia

[1] Erich Gamma, Ricard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).

[2] Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra. Head First Design Patterns. O'Reilly Media, 2004.

[3] Deepak, A.; Dan, M.; John, C.; Core J2EE Patterns: Best Practices and Design Strategies (2nd Edition). Prentice Hall, 2003.