Strategy Pattern

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.

LoginController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LoginController : Controller {

public ActionResult Index() {
return View();
}

[HttpPost]
public ActionResult Index(LoginViewModel viewModel) {
if (!ModelState.IsValid) return View(viewModel);

var username = viewModel.Username;
var password = viewModel.Password;

var application = new UserApplication();
var valid = application.Validate(username, password);

//Caso o usuario nao seja valido, retorna para a view.
if (!valid) return View(viewModel);

//Autentica o usuario no sistema.
}
}

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

UserApplication.cs
1
2
3
4
5
6
7
8
9
10
public class UserApplication {

public bool Validate(string username, string password) {
var userDAO = new UserDAO();
var user = userDAO.GetBy(username);
if (user == null) return false;

return user.Check(password);
}
}

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

UserDAO.cs
1
2
3
4
5
6
public class UserDAO {

public User GetBy(string username) {
throw new System.NotImplementedException();
}
}

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.

UserApplication.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserApplication {

private readonly IUserDAO _userDAO;

public UserApplication(IUserDAO userDAO) {
_userDAO = userDAO;
}

public bool Validate(string username, string password) {
var user = _userDAO.GetBy(username);
if (user == null) return false;

return user.Check(password);
}
}

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.

UserDAO.cs
1
2
3
4
5
6
7
8
9
10
11
public interface IUserDAO {

User GetBy(string username);
}

public class UserDAO : IUserDAO {

public User GetBy(string username) {
throw new System.NotImplementedException();
}
}

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.

LoginController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class LoginController : Controller {

public ActionResult Index() {
return View();
}

[HttpPost]
public ActionResult Index(LoginViewModel viewModel) {
if (!ModelState.IsValid) return View(viewModel);

var username = viewModel.Username;
var password = viewModel.Password;

IUserDAO dao = new UserDAO();
var application = new UserApplication(dao);
var valid = application.Validate(username, password);

//Caso o usuario nao seja valido, retorna para a view.
if (!valid) return View(viewModel);

//Autentica o usuario no sistema.
throw new NotImplementedException();
}
}

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.

FakeUserDAO.cs
1
2
3
4
5
6
7
8
public class FakeUserDAO : IUserDAO {

public User User { get; set; }

public User GetBy(string username) {
return User;
}
}

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

UserApplicationTest.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[TestFixture]
public class UserApplicationTest {

private const string Username = "username";
private const string Password = "password";

private FakeUserDAO _fakeUserDAO;

[SetUp]
public void SetUp() {
_fakeUserDAO = new FakeUserDAO();
}

[Test]
public void Validate_QuandoUsuarioNaoExiste_RetornaFalse() {
var userApplication = new UserApplication(_fakeUserDAO);
var userIsValid = userApplication.Validate(Username, Password);

userIsValid.Should().Be.False();
}

[Test]
public void Validate_QuandoASenhaEhInvalida_RetornaFalse() {
_fakeUserDAO.User = new User { Password = "pass" };

var userApplication = new UserApplication(_fakeUserDAO);
var userIsValid = userApplication.Validate(Username, Password);

userIsValid.Should().Be.False();
}

[Test]
public void Validate_QuandoASenhaEhValida_RetornaTrue() {
_fakeUserDAO.User = new User { Password = Password };

var userApplication = new UserApplication(_fakeUserDAO);
var userIsValid = userApplication.Validate(Username, Password);

userIsValid.Should().Be.True();
}
}

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.