NET - Padrões de projeto - Usando Strategy


Para uma introdução conceitual sobre Padrões de Projetos veja as referências a meus artigos no final do artigo.

Hoje eu vou falar um pouco sobre o padrão Strategy porque ele é muito importante e pode lhe ajudar muito no processo de desenvolvimento de software.

Vou começar com apresentando algumas definições clássicas do padrão :

"O padrão Strategy define uma família de algoritmos , encapsula cada um deles , e torna-os intercambiáveis, permitindo assim que os algoritmos variem independentemente dos clientes que os utilizam"

ou em outra palavras...

"O padrão Strategy define uma familia de algoritmos intercambiáveis de forma que estes sejam independentes dos clientes que os utilizam. "

Resumindo:

Objetivo: Encapsular um algoritmo em um objeto.
Fornecer interfaces genéricas o suficiente para suportar uma variedade de algorítmos.
Facilitar a escolha e troca (intercâmbio) de algoritmos criados com uma mesma função.

O pulo do gato para usar o padrão Strategy é perceber o que pode mudar no seu código e encapsular.

Vejamos alguns cenários onde o padrão pode ser aplicado:

Esquema UML do padrão:

Strategy - Interface comum para todas as classes (variações concretas) que definem os diversos comportamentos esperados;

ConcreteStrategy - Classes que implementam os algoritmos que devem atender a cada contexto;

Context - Classe onde os objetos ConcreteStrategy serão instanciados;

- As classes Context instanciam os objetos Strategy e invocam o método AlgorithmInterface passando os parâmetros solicitados;
- A interface Strategy decide qual das implementações ConcreteStrategy deve atender a chamada;

Esse padrão tem como elementos participantes o Context, que tem seu "comportamento" ou parte dele definido pelo algoritmo implementado pela Strategy referenciada; o Strategy, que define a interface comum para todos os algoritmos; o ConcreteStrategy, que implementa os algoritmos definidos pela interface Strategy.

O padrão Strategy te conduz a seguinte orientação:

1 - Programe sempre para interfaces;
2 - Dê preferência a composição ao invés de herança;

Nota: Para quem começa a aprender os conceitos da programação orientada a objetos(OOP) pode ficar encantado com o recurso da herança, afinal , e só sair herdando as classes para reutilizar código.(pura preguiça...)

Mas a herança(É-UM) é como uma teia de aranha , se você cair nela , pode se embaraçar cada vez mais a cada tentativa de sair.

Para usar herança você tem que planejar muito bem a sua hierarquia de classes pois herança mal definida pode quebrar o encapsulamento e causar um forte acoplamento. Você altera uma classe A que herda de B que por sua vez herda de C , e assim por diante; no fim você se dá conta do emaranhado de implicações que uma simples alteração naquela 'inocente' classe A pode causar devido a dependência das implementações que foram herdadas. (planejar é preciso my friend...)

Desta forma , em muitos casos é preferível usar composição(TEM-UM) ao invés de herança(É-UM). (veremos isso no padrão Strategy)

Como estamos lidando com conceitos abstratos não é tão fácil como parece implementar o padrão no seu dia a dia. Você pode até entender o conceito mas somente a prática e o bom senso fará com que você saiba identificar a situação na qual o padrão deve ser usado.

Vou tentar mostrar a seguir alguns exemplos de utilização do padrão Strategy. Os exemplos eu encontrei na web e fiz algumas adaptações para a plataforma .Net.

A seguir eu vou mostrar um exemplo usado para o Java que eu adaptei e simplifiquei mais ainda para o VB  .NET de autoria de Fabio Santiago. Ele é usado com objetivo didático de apresentar o padrão Strategy.

Cenário:

Imagine que temos um pequeno sistema que apresenta uma classe abstrata Documento que possui um método chamado imprimir();

Imagine também que temos duas classes :

  1. Legal - que herda da classe Documento e sobrescreve o método imprimir() para imprimir documentos Legais;
  2. Tecnico - que também herda de Documento e sobrescreve o método imprimir() para imprimir documentos técnicos;

Perceba que eu poderia ter muitas outras classes herdando de documento e cada uma sobrescrevendo o método imprimir() para atender um requisito de impressão diferente como : HTML , PDF, TEXTO, RTF , etc.

 

O código para a classe no VB .NET 2008 seria:

Abra o VB 2008 e crie um novo projeto do tipo Console Application com o nome pEstrategia e a seguir inclua um novo item Class chamado Documento.vb:

Namespace pEstrategia
 

Public MustInherit Class Documento


    Public
Sub
imprimir()

        Console.WriteLine("Imprimir")

    End Sub

 

End Class
 

End Namespace

Lembre-se que uma classe abstrata não pode ser instanciada, mas:
  • Pode ter implementação básica de métodos;
  • Pode ter variáveis;
  • Pode definir modificadores de acesso a seus métodos;
  • Classes só podem derivar de uma abstrata;

 

A seguir inclua mais dois novos itens Class na solução, um com o nome Legal.vb e outro com o nome Tecnico.vb contendo o seguinte código:

Namespace pEstrategia

 

Public Class Legal

           Inherits Documento

End Class

 

End Namespace

Namespace pEstrategia

 

Public Class Tecnico

        Inherits Documento

End Class

End Namespace

Legal.vb

Tecnico.vb

Você já deve ter percebido que o método imprimir() vai ser implementado de forma diferente para cada requisito de impressão e vai ter que ser sobrescrito de forma distinta em cada uma das classes que herdam da classe Documento.

Temos então na solução apresentada dois grandes problemas:

Aqui é que entra o padrão Strategy ,e, para aplicá-lo vamos usar as premissas:

1 - Programe sempre para interfaces;
2 - Dê preferência a composição ao invés de herança;

Vamos fazer isso removendo o método imprimir(), pois ele representa o algoritmo que esta variando, da classe Documento e criar uma interface chamada IImprimir na solução definindo a assinatura do método imprimir():

Namespace pEstrategia

Public Interface IImprimir
        Sub
Imprimir()
End
Interface

End Namespace

A seguir vamos criar na solução duas novas classes que representam uma particularidade de impressão e que irão implementar a interface IImprimir(). Vamos criar as classes PDF.vb e HTML.vb implementando a interface criada:

Namespace pEstrategia


Public Class PDF

      Implements IImprimir
 

    Public Sub imprimir() Implements IImprimir.Imprimir

          Console.WriteLine("imprimindo PDF")

      End Sub

End Class
 

End Namespace

Namespace pEstrategia

 

Public Class HTML

       Implements IImprimir
 

       Public Sub imprimir() Implements IImprimir.Imprimir

           Console.WriteLine("imprimindo HTML")

          End Sub

End Class

End Namespace

PDF.vb

HTML.vb

Temos agora duas classes que estão implementando a partir da interface IImprimir a impressão de acordo com sua particularidade vamos então remover o método imprimir() da classe Documento e efetuar alguns ajustes conforme abaixo:

Namespace pEstrategia
 

Public MustInherit Class Documento


   Protected
iimprimir As IImprimir


    Public
Sub executaImprimir()

        iimprimir.Imprimir()

    End Sub
 

       Public Sub setImprimir(ByVal iimprimir As IImprimir)

          Me.iimprimir = iimprimir

    End Sub
 

End Class
 

End Namespace

Criamos o atributo de instância iimprimir que diz respeito ao comportamento IImprimir onde temos a assinatura do método Imprimir();

Definimos o método executaImprimir() que usa o método imprimir definido na interface IImprimir(), dessa forma o método irá usar os serviços do comportamento da instância iimprimir para imprimir;

Não temos conhecimento do comportamento IImprimir ele é transparente para a classe Documento;

Não temos mais a implementação para imprimir na classe , ela foi separada e definida na interface IImprimir;

Temos desta forma um baixo acoplamento entre o Documento e o comportamento (a in terface) onde definimos o método Imprimir();

Para mudar o comportamento em tempo de execução definimos o método setImprimir();

Antes a definição para imprimir estava definida na classe de forma fixa e não podia ser alterada;

Finalmente para testar a estratégia vamos definir o código abaixo no módulo da solução:

Namespace pEstrategia
 

     Module Module1


         Sub
Main()

              Dim doc As Documento = New Legal()

              doc.setImprimir(New PDF())

              doc.executaImprimir()

              doc.setImprimir(New HTML())

              doc.executaImprimir()

          End Sub

         End Module
 

End Namespace

No código acima criamos uma instância da classe Legal e imprimimos PDF; em seguida imprimimos HTML provando que podemos alterar o comportamento em tempo de execução.

A seguir estou mostrando o diagrama das classes usadas , eu obtive este diagrama através do BlueJ, um aplicativo para Java;

Para ajudar na compreensão do padrão vou apresentar um problema mais prático e como a aplicação do padrão poderá resolvê-lo. Neste exemplo vou usar a linguagem C# e criar o diagrama de classes no Visual Studio 2005.

Imagine que uma loja pretende aplicar uma política de descontos no preço dos seus produtos bem agressiva onde cada produto poderá, de acordo com as datas especiais do calendário: dia das mães , dia dos pais , dia das crianças , páscoa, natal , etc..., ter um preço promocional.

Neste cenário teríamos para cada data um tipo de desconto diferente que deverá ser implementado por um algoritmo diferente.

1- Primeiro definimos uma interface chamada IPromocao contendo a definição de um método chamado desconto();

As interfaces não podem ter variáveis, nem ter implementações básicas e nem possuir modificadores de acesso nos seus métodos dela; quem deve fazer isso é a classe que a implementa.

namespace StrategyVendas

{

    public interface IPromocao

    {

          decimal desconto();

    }

}

2- Em seguida criamos duas classes que irão implementar esta interface efetuando um desconto para uma das datas especiais. Como exemplo usei os nomes PromocaoNatal e PromocaoNamorados:

Namespace StrategyVendas

{

     class PromocaoNatal : IPromocao{

           public decimal desconto(){

                 return 10;

        }

    }

}

namespace StrategyVendas

{

      class PromocaoNamorados : IPromocao{

           public decimal desconto(){

                  return 15;

         }

     }

}

3- Definimos uma classe abstrata chamada Produto que irá retornar o resultado do método desconto da Interface definida:

namespace StrategyVendas

{

    public abstract class Produto{


       protected
IPromocao promocao;


       public
decimal desconto(){

            return promocao.desconto();

        }

    }

}

4- Finalmente criamos as classes DVD.cs e Celular.cs para cada produto que herda de Produto e define um tipo de promoção diferente. Havendo uma nova promoção basta definir a nova classe para a promoção e a classe do produto usar a nova promoção:

namespace StrategyVendas

{

    public class DVD : Produto{
        public DVD(){

            this.promocao = new PromocaoNatal();

        }

    }

}

namespace StrategyVendas

{

       public class Celular : Produto{

          public Celular(){

            this.promocao = new PromocaoNamorados();

          }

       }

}

A seguir temos o Diagrama de classes para as classes descritas acima :

Resumindo:

  1. Identifique um algoritmo, ou seja um comportamento, que deverá ser usado por um cliente;
  2. Defina uma assinatura para o algoritmo em uma interface;
  3. Efetua os detalhes da implementação em classes derivadas que implementam a interface;
  4. Defina classes concretas no cliente para usar o algoritmo;

A palavra final:

O padrão Strategy é um dos padrões que pode ser usado para otimizar e facilitar a manutenção do seu código. Espero que o artigo o auxilie a compreender este importante padrão.

O código usado pode ser baixado aqui : pEstrategiaVB.zip ou  pEstrategiaC#.zip

Referências:


José Carlos Macoratti