20 de abr. de 2010

Padrões de Projeto ( Design Patterns ) - O que são?



Padrões de Projeto (os Design Patterns), são combinações de classes e algoritmos associados que cumprem com propósitos comuns de projeto. São normalmente soluções consagradas que se baseiam nas estruturas da orientação a objetos em sua melhor forma.

Cada padrão descreve um problema no nosso ambiente e o cerne da sua solução, de tal forma que você possa usar essa solução mais de um milhão de vezes, sem nunca fazê-lo da mesma maneira.

Os padrões de projeto, basicamente, se dividem em grandes 3 categorias: Criacional, Estrutural e Comportamental.
 
São 4 os elementos importantes que definem um Padrão (Pattern), são eles:

  • Nome: A identificação do Pattern é importante pois ele torna-se membro do vocabulário do projetista e de seus colegas; 
  • Problema: descreve quando aplicar o Pattern. Apresenta a classe de problemas em questão e seu contexto; 
  • Solução: descreve os elementos que fazem parte do design, seus relacionamentos, responsabilidades e colaborações; 
  • Conseqüências: os resultados e efeitos causados pela aplicação do pattern.

Para se descrever e catalogar um Padrão de Projeto com total completude de informações, são necessários os seguintes dados:

  • Nome do Pattern e Classificação: Passa a fazer parte do vocabulário dos projetistas;
  • Propósito: Respostas para as perguntas - O quê o Pattern faz? Que tipo de problema ou característica particular de Design ele trata?
  • Também Conhecido Como: Conjunto de outros nomes (apelidos) conhecidos para o Pattern, se existir algum;
  • Motivação: Um cenário que ilustra o problema de Design e como as estruturas de classes e objetos no Pattern o resolvem;
  • Aplicação: Respostas para as perguntas - Quais são as situações onde este Pattern pode ser aplicado? Quais são os exemplos de Designs que ele pode tratar? Como você pode reconhecer estas situações? 
  • Estrutura: Uma representação gráfica das classes no Pattern. Participantes: As classes e/ou objetos que participam no Design Pattern, e suas responsabilidades;
  • Colaborações: Como os participantes interagem para cumprir suas responsabilidades;
  • Conseqüências: Respostas para as perguntas - Como o Pattern alcança seus objetivos? Quais são os resultados do uso do Pattern?
    Implementação: Dicas e técnicas que Designer deve saber, e possíveis armadilhas para as quais ele deve estar preparado;
  • Código Exemplo: Fragmentos de código que ilustrem como o Pattern deve ser implementado;
    Usos Conhecidos: Exemplos de utilização do Pattern em sistemas já implementados;
  • Patterns Relacionados: Lista de todos os Patterns fortemente relacionados ao Pattern em questão e as suas principais diferenças.

Atualmente existem diversos catálogos de padrões já criados,  dentre os grandes, posso citar alguns aqui:
  • Padrões Gof 
  • Padrões JEE 
  • Padrões Microsoft
  • Padrões Corporativos 
  • Padrões de Integração 
  • Padrões SOA

Os Patterns são oriundos de alguns princípios de programação, como por exemplo, o Design de Objetos, o que se torna algo complexo em projetos de software, já que muitos parâmetros estão implícitos neste conceito, como encapsulamento, granularidade, dependência, flexibilidade, desempenho, evolução e reutilização.

Muitos objetos oriundam da análise, e muitos destes não são abstrações do mundo real, como vetores por exemplo. Por outro lado, existem objetos que raramente aparecem durante a análise, e sim, aparecem para auxiliarem a fase de projeto. Logo, os padrões de projeto ajudam a identificar abstrações menos óbvias bem como os objetos que podem captura-las. Padrões como Composite (tratamento uniforme de objetos que não têm uma contrapartida física), Strategy (descreve como implementar famílias de algoritmos intercambiáveis) e State (representa cada estado de uma entidade como um objeto) são bastante úteis, como exemplo.

A granulidade de objetos:

Objetos podem variar tremendamente em tamanho e número. Podem representar qualquer abstração. Os padrões de projeto auxiliam no tratamento da granularidade. Tem-se como exemplo:

Façade: Descreve como representar subsistemas completos como objetos;

Flyweight: Descreve como suportar enormes quantidades de objetos nos níveis de granularidade mais finos;

Abstract Factory: Fornecem objetos cujas únicas responsabilidades são criar outros objetos;

Command: Fornecem objetos cujas únicas responsabilidades são implementar uma solicitação em outro objeto ou grupo de objetos.

Especificação de Interfaces: 

Um bom conceito O.O para se programar utilizando padrões de projeto é: "Programa para interfaces, nunca para uma implementação".

Uma interface é o conjunto de assinaturas declaradas por um objeto, sendo que cada operação declarada (Assinatura) especifica o nome da operação, os objetos que ela aceita como parâmetros e o valor retornado por ela.

Um tipo é um nome usado para denotar uma interface específica. Quando um objeto tem um tipo “Janela”, significa que ele aceita todas as solicitações para as operações definidas na interface chamada “Janela”.
As interfaces são fundamentais para a programação orientada à objetos. Objetos devem ser conhecidos somente por suas interfaces e, quando uma mensagem é enviada a um objeto, a operação específica que será executada depende da “mensagem” e do objeto “receptor”. Na prática, significa que diferentes objetos que suportam solicitações idênticas, podem possuir diferentes implementações para atender estas solicitações, é o chamado Polimorfismo entrando em cena!

A associação em tempo de execução de uma solicitação a um objeto e a uma das suas operações é conhecida como ligação dinâmica (dynamic binding). Essa ligação auxilia o polimorfismo que por sua vez, simplifica as definições dos clientes, desacopla objetos entre si e permite a eles variarem seus inter-relacionamentos em tempo de execução. Os padrões de projeto auxiliam o uso do polimorfismo bem como o uso de interfaces, além de ajudarem a definir as interfaces pela identificação de seus elementos-chave e pelos tipos de dados que são enviados através das assinaturas das operaões contidas nas mesmas.

Classe do objeto (implementação) x Tipo do objeto (interface): um objeto pode ter muitos tipos, e objetos de diferentes classes podem ter o mesmo tipo;

Herança de classe x Herança de interface: a herança de classe define a implementação de um objeto em termos da implementação de outro objeto (é um mecanismo para compartilhamento de código e de representação), já a herança de interface descreve quando um objeto pode ser usado no lugar de outro.

Dado todo este contexto, uma outra dica O.O importante é: não declarar variáveis como instâncias de classes concretas específicas. Isso dá maior flexibilidade ao código e ajudará no polimorfismo.


Herança e Composição:

As vantagens de se utilizar herança nos seus projetos O.O. basicamente são: simplicidade e rapidez na execução (definida em tempo de compilação) e que a mesma permite alterar com facilidade a implementação herdada (reuso). 

Já entre as desvantagens, podemos citar: a herança não permite alterar a implementação herdada em tempo de execução e expõe para as classes descendentes, os detalhes da implementação, ferindo assim, o princípio do encapsulamento. A dependência da implementação herdada também prejudica no reuso das classes. 

As vantagens da Composição basicamente são: os objetos são acessados somente através de suas interfaces, sem violar o encapsulamento e proporcionando independência de implementação, além do fato de que, com classes em que um número pequeno de heranças é usado e temos papéis bem definidos e focados, aumenta-se então, a capacidade de reutilização.

As desvantagens da Composição podem ser: como neste caso a herança diminuirá, e o número de objetos no sistema aumentará, e consequentemente, a performance do sistema que utiliza a composição dependerá de como será feito o inter-relacionamento entre esses  objetos.


Delegação:

É um princípio muito importante e que também é largamente utilizado nos Padrões de Projetos, se baseia no envolvimento de dois objetos ou mais objetos que visam atender a uma determinada solicitação (utiliza poderosamente a composição de objetos).

Exemplificando-se tem-se uma classe “Window” em “Rectangle”. Em vez de se fazer “Window” sub-classe de “Rectangle” (uma Janela é retangular!), ela reutilizaria o comportamento de “Rectangle”. Em vez de dizer que uma “Window” é um “Rectangle”, diremos então que uma “Window” possui um comportamento de um “Rectangle”.

A principal vantagem disso é que se “Window” em tempo de execução, quiser ter um comportamento de um “Circle” (círculo), basta utilizar uma instância de “Circle” assumindo que ambas tenham o mesmo tipo (interface).

Existem duas desvantagens: Um código dinâmico e altamente parametrizável é mais difícil de compreender e a performance é comprometida porque definir trocas em tempo de execução possuem um “overhead” da troca de contexto antes da execução propriamente dita.

Conclui-se que o uso da delegação é recomendável quando a mesma simplificar e não complicar o projeto.


Um exemplo de Padrão para ilustrar esses conceitos: Bridge.




Este padrão estrutural do GOF basicamente define um modelo no qual separa o conceito (definido pelas classes à esquerda) das suas implementações.

A herança é utilizada nas relações entre as classes Abstraction e RefinedAbstraction, na qual uma implementação do conceito é definida pelo seu tipo. 

Temos herança também na relação entre a interface Implementor e suas implementações ConcreteImplementorA e ConcreteImplementorB. O propósito disto é dar flexibilidade na relação entre o conceitos e suas duas formas de implementações, configurando assim a relação de delegação polimórfica entre as duas estruturas definidas distintamente.

A composição se dá justamente nesta relação de delegação entre as duas interfaces Abstraction e Implementor, o que nos permite compor N combinações de novas abstrações (conceitos) com N tipos diferentes de comportamentos (implementações). Tal definição nos fornece então, a granularidade de objetos almejada para este propósito.

Podemos concluir então que, RefinedAbstraction pode invocar métodos, assumindo comportamentos, (representados por sua interface) tanto de ConcreteImplementorA ou ConcreteImplementorB

Bom, neste ponto você já deve estar pensando que poderia muito bem fazer esta relação entre conceito <-> implementação simplesmente colocando as classes ConcreteImplementorA e ConcreteImplementorB como "herdeiras" de RefinedAbstraction, mas esta falta de separação te dará algumas dores de cabeça quando você necessitar incluir mais uma forma de implementação (ConcreteImplementorC, por exemplo), já que, para se adequar a isto, teria-se que modificar também a RefinedAbstraction, ou seja, o custo de manutenção, em um projeto real, vai aumentando exponencialmente e, certamente você não vai querer isso!

É isso aí pessoal, concluo o meu post aqui e, espero ter esclarecido em poucas palavras, o que são os padrões de projeto e os conceitos e definições que o envolvem.

Fiquem à vontade para comentar, tirar dúvidas ou sugerir melhorias.

Um grande abraço a todos e até o próximo post!


.