Júnior Oliveira

C Sharp; Delphi; ASP .NET; PHP; Javascript;

Strategy Pattern

30 Oct 2012 csharp, design pattern, di, ioc

Olá pessoal, vou falar um pouco sobre Strategy Pattern, é um Design Pattern que ficou famoso depois de ser catalogado pelo GoF (Gang Of Four) formado por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides no livro "Design Patterns: Elements of Reusable Object-Oriented Software".

Segue abaixo a definição de acordo com o livro:

"Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it."

Vamos ao exemplo para ficar mais claro a definição.

Imagina que você precisa desenvolver uma funcionalidade que irá validar um usuário. Este é um cenário bem simples e típico podemos fazer esta implementação de várias formas diferentes, abaixo segue um código que em um primeiro momento acho que seria o mais comum.

A primeira classe é uma controller do asp.net mvc que terá uma action que irá receber o usuário e a senha por POST e chamar a classe que irá validar estas informações.

 1 public class LoginController : Controller {
 2 
 3     public ActionResult Index() {
 4         return View();
 5     }
 6 
 7     [HttpPost]
 8     public ActionResult Index(LoginViewModel viewModel) {
 9         if (!ModelState.IsValid) return View(viewModel);
10 
11         var username = viewModel.Username;
12         var password = viewModel.Password;
13 
14         var application = new UserApplication();
15         var valid = application.Validate(username, password);
16 
17         //Caso o usuario nao seja valido, retorna para a view.
18         if (!valid) return View(viewModel);
19 
20         //Autentica o usuario no sistema.
21     }
22 }

A classe abaixo será responsável por controlar o fluxo de validação do usuário.

 1 public class UserApplication {
 2 
 3     public bool Validate(string username, string password) {
 4         var userDAO = new UserDAO();
 5         var user = userDAO.GetBy(username);
 6         if (user == null) return false;
 7 
 8         return user.Check(password);
 9     }
10 }

Nossa classe que será responsável por manipular os dados do usuário.

1 public class UserDAO {
2 
3     public User GetBy(string username) {
4         throw new System.NotImplementedException();
5     }
6 }

Como mencionado acima, este é um código bem simples, porém, este código é impossível testar unitariamente e caso haja a necessidade de alterar a fonte dos dados também será difícil.

Para resolver este problema, podemos aplicar o Strategy Pattern, de forma que iremos passar para a classe UserApplication a "estratégia" (classe) de acesso a dados que iremos utilizar, assim fica fácil testar a classe UserApplication unitariamente e de mudar a fonte de dados quando necessário.

 1 public class UserApplication {
 2 
 3     private readonly IUserDAO _userDAO;
 4 
 5     public UserApplication(IUserDAO userDAO) {
 6         _userDAO = userDAO;
 7     }
 8 
 9     public bool Validate(string username, string password) {
10         var user = _userDAO.GetBy(username);
11         if (user == null) return false;
12 
13         return user.Check(password);
14     }
15 }

Notem que agora eu passo a dependência do repositório no construtor da minha classe UserApplication, sendo assim eu consigo passar qualquer classe que implemente a classe IUserDAO.

Abaixo notem que a nossa classe UserDAO sofreu uma modificação, agora ela implementa a interface IUserDAO sendo assim eu posso passar ela no construtor da nossa classe UserApplication.

 1 public interface IUserDAO {
 2 
 3     User GetBy(string username);
 4 }
 5 
 6 public class UserDAO : IUserDAO {
 7 
 8     public User GetBy(string username) {
 9         throw new System.NotImplementedException();
10     }
11 }

Vocês podem perceber que agora estou passando para a classe UserApplication a conexão que ela deverá utilizar. Esta classe de conexão é uma "estratégia" que estou utilizando para acessar as informações do usuário, caso haja necessidade de acessar as informações de outra fonte de dados é necessário apenas implementar outra "estratégia" (classe) que implemente a interface IUserDAO e passar para a nossa classe UserApplication como mencionado acima.

 1 public class LoginController : Controller {
 2 
 3     public ActionResult Index() {
 4         return View();
 5     }
 6 
 7     [HttpPost]
 8     public ActionResult Index(LoginViewModel viewModel) {
 9         if (!ModelState.IsValid) return View(viewModel);
10 
11         var username = viewModel.Username;
12         var password = viewModel.Password;
13 
14         IUserDAO dao = new UserDAO();
15         var application = new UserApplication(dao);
16         var valid = application.Validate(username, password);
17 
18         //Caso o usuario nao seja valido, retorna para a view.
19         if (!valid) return View(viewModel);
20 
21         //Autentica o usuario no sistema.
22         throw new NotImplementedException();
23     }
24 }

Abaixo outra "estratégia" (classe) de acesso a dados que irá conter os métodos que simulará as operações na base de dados, nos ajudando assim a testar a classe UserApplication.

1 public class FakeUserDAO : IUserDAO {
2 
3     public User User { get; set; }
4 
5     public User GetBy(string username) {
6         return User;
7     }
8 }

O teste abaixo é para garantir que as operações da classe UserApplication estão de acordo com as regras do negócio.

 1 [TestFixture]
 2 public class UserApplicationTest {
 3 
 4     private const string Username = "username";
 5     private const string Password = "password";
 6 
 7     private FakeUserDAO _fakeUserDAO;
 8 
 9     [SetUp]
10     public void SetUp() {
11         _fakeUserDAO = new FakeUserDAO();
12     }
13 
14     [Test]
15     public void Validate_QuandoUsuarioNaoExiste_RetornaFalse() {
16         var userApplication = new UserApplication(_fakeUserDAO);
17         var userIsValid = userApplication.Validate(Username, Password);
18 
19         userIsValid.Should().Be.False();
20     }
21 
22     [Test]
23     public void Validate_QuandoASenhaEhInvalida_RetornaFalse() {
24         _fakeUserDAO.User = new User { Password = "pass" };
25 
26         var userApplication = new UserApplication(_fakeUserDAO);
27         var userIsValid = userApplication.Validate(Username, Password);
28 
29         userIsValid.Should().Be.False();
30     }
31 
32     [Test]
33     public void Validate_QuandoASenhaEhValida_RetornaTrue() {
34         _fakeUserDAO.User = new User { Password = Password };
35 
36         var userApplication = new UserApplication(_fakeUserDAO);
37         var userIsValid = userApplication.Validate(Username, Password);
38 
39         userIsValid.Should().Be.True();
40     }
41 }

Analisando a definição do pattern, descrita no inicio do post:

O contexto é a classe UserApplication.

As estratégias criadas foram as classes que implementam a interface IUserDAO no caso a classe UserDAO e FakeUserDAO.

E os clientes que utilizam a classe UserApplication são os testes da classe UserApplication e a action da LoginController.

Neste post usei os seguintes pacotes: Sharp Tests Ex e NUnit.

Existem outras aplicações mais comuns para o Strategy Pattern como: criptografias, logs entre outros cenários, escolhi este cenário também para demonstrar a aplicação de testes unitários em códigos "legados".

Espero que tenham gostado do exemplo e até a próxima.