Design Patterns — Builder

Gabriel Muniz
3 min readApr 29, 2022

--

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.

--

--