Design Patterns — Builder
Os padrões de projetos nada mais são que formas comprovadamente válidas para solucionar problemas recorrentes dentro do universo do desenvolvimento de software.
Podemos dividir estes padrões em vários escopos, tais como: padrão criacional, estrutural e comportamental. Mais detalhes sobre a definição de GoF podem ser encontrados no livro Padrões de Design: Elementos de Software Orientado a Objetos Reutilizáveis (Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides). Entrando primariamente nos padrões criacionais temos um padrão muito útil e que nos auxilia a tornar nosso código facilmente testável. Vamos a um rápido exemplo de aplicação do padrão builder.
Builder Pattern
O padrão builder auxilia o desenvolvedor (como o próprio nome sugere) na criação de objetos complexos step by step, ou propriedade a propriedade. Vejamos um problema claro que pode ser resolvido através do padrão:
public class User
{
private string PASSWORD_PATTERN = "[0-9]+"; public Guid Id { get; }
public string UserName { get; set; }
public string FullName { get; set; }
public Profile Profile { get; set; }
public string Password { get; set; } public User(string userName, string fullName, string password, Profile profile)
{
if (IsValid(userName, fullName, password, profile))
{
Id = Guid.NewGuid();
UserName = userName;
FullName = fullName;
Password = password.ToMd5();
Profile = profile;
}
} private bool IsValid(string userName, string fullName, string password, Profile profile)
{
return ProfileNotNull(profile) && IsValidUserName(userName) && IsValidFullName(fullName) &&
IsValidPassword(password);
}
}
Perceba que o construtor chama um método de validação que passa por todas propriedades da entidade.
Como podemos testar a criação dessa entidade? Veja o exemplo de teste unitário.
public class UserTest
{
[Fact]
public void DeveCriarUsuarioComNomeDeUsuarioCorreto()
{
var user = new User("joaoSilva", "João da Silva", "1234", new Profile());
Assert.Equal("gabrielMuniz", user.UserName);
}
[Fact]
public void DeveCriarUsuarioComNomeCompletoCorreto()
{
var user = new User("joaoSilva", "João da Silva", "1234", new Profile());
Assert.Equal("João da Silva", user.FUllName);
}
[Fact]
public void DeveCriarUsuarioComNomeCompletoCorreto()
{
var user = new User("joaoSilva", "João da Silva", "1234", new Profile());
Assert.Equal("905669063311D8A17BD6958CD353EEDD", user.Password);
}
}
Podemos executar testes unitários em cima do construtor, porém será que eles fazem sentido?
Notou? Quando executamos um teste unitário DeveCriarUsuarioComNomeDeUsuarioCorreto, por conter uma validação dentro do construtor da entidade, a criação percorre também validações que em nada tem a ver com o nome de usuário. Ou seja, é um teste unitário que foge o princípio de testar a menor unidade possível de código, certo? Como podemos resolver esse problema através do padrão builder? Vamos refatorar a entidade para propiciar isso.
namespace PostsApp.Domain.Factories
{
public class UserBuilder
{
private string PASSWORD_PATTERN = "[0-9]+"; private User _user; public UserBuilder CreateUser()
{
_user = new User();
return this;
} public User Generate() => _user; public UserBuilder WithFullName(string fullName)
{
_user.FullName = fullName.Length <= 50
? fullName
: throw new InvalidUserException("Nome completo deve ter no máximo 50 caracteres.");
return this;
} public UserBuilder WithUserName(string userName)
{
_user.UserName = userName.Length >= 10
? userName
: throw new InvalidUserException("Usuário deve possuir ao menos 10 caracteres.");
return this;
} public UserBuilder WithPassword(string password)
{
_user.Password = Regex.Match(password, PASSWORD_PATTERN).Success
? password.ToMd5()
: throw new InvalidUserException("Senha não atende os critérios de segurança.");
return this;
} public UserBuilder WithProfile(Profile profile)
{
_user.Profile = profile ?? throw new InvalidUserException("Usuário não possui perfil vinculado.");
return this;
}
}
}
Melhor, não? Agora temos cada método With construindo pequenas frações da entidade, facilitando os testes de unidade e nos ajudando a validar apenas o necessário em cada teste. Vamos refatorar os testes? Segue:
namespace PostsApp.UnitTests
{
public class UserTest
{
[Fact]
public void DeveCriarUsuarioComNomeDeUsuarioCorreto()
{
var user = new UserBuilder()
.CreateUser()
.WithUserName("gabrielMuniz")
.Generate();
Assert.Equal("gabrielMuniz", user.UserName);
} [Fact]
public void DeveCriarUsuarioComNomeCompletoCorreto()
{
var user = new UserBuilder()
.CreateUser()
.WithFullName("Gabriel Silvano Muniz")
.Generate();
Assert.Equal("Gabriel Silvano Muniz", user.FullName);
} [Fact]
public void DeveCriarUsuarioComSenhaCorreta()
{
var user = new UserBuilder()
.CreateUser()
.WithPassword("1234")
.Generate();
Assert.Equal("81DC9BDB52D04DC20036DBD8313ED055", user.Password);
}
}
}
Veja, agora quando executamos o teste DeveCriarUsuarioComNomeDeUsuarioCorreto validamos apenas a criação do nome de usuário e assim sucessivamente.
É claro que este não é o único benefício da utilização deste padrão e ele pode/deve ser utilizado em conjunto com outros padrões que soam como música ao resolver determinados problemas. O próximo padrão de criação que vamos abordar será o factory method. Então até lá!
IMPORTANTE: Tome muito cuidado com a febre do patternite. Nem todo padrão é aplicável ou saudável para seu código em qualquer situação, é importante ter uma análise sincera do que é realmente necessário.