Alguns anos atrás, quando eu comecei a aprender Scala, eu li um post muito interessante sobre como criar um builder typesafe em Scala. Depois de conversar com o Abner das Dores, chegamos à conclusão de que é possível fazer algo semelhante em C#.
O problema
Pense em um builder para construir uma pessoa com nome e endereço:
public class Pessoa { } public class Builder { public Builder ComNome(String s) { return new Builder(); } public Builder ComEndereco(String s) { return new Builder(); } public Pessoa build() { return new Pessoa(); } }
O código é simples e funcional. Mas ele permite coisas como:
public class FormaDe { public void uso() { new Builder().Build(); // Isso não deveria ser permitido new Builder().ComNome("").Build(); // Nem isso new Builder().ComEndereco("").Build(); // Ou isso new Builder().ComNome("").ComEndereco("").Build(); // ok new Builder().ComEndereco("").ComNome("").Build(); // ok } }
Como o nosso builder permite o uso errado dele, fica a cargo do programador ter que lembrar como usá-lo. Como eu sempre esqueço como usar as coisas, prefiro que o código não compile quando eu escrevo código errado. O nosso problema é que o método Build
está sempre disponível. Temos que dar um jeito de controlar melhor quando ele está disponível.
Colocando o método Build “manualmente”
C# possui um recurso bem interessante que é meio caminho para a nossa solução. Com o uso de Extension Methods, nós podemos fazer com que pareça que o método Build
está na nossa classe Builder
, mas ele está em outra classe onde temos mais controle.
public class Pessoa { } public class Builder { public Builder ComNome(String s) { return new Builder(); } public Builder ComEndereco(String s) { return new Builder(); } } public static class Extension { public static Pessoa Build(this Builder builder) { return new Pessoa(); } }
Por enquanto nada mudou. O método Build
continua sempre disponível. O que nos leva à próxima parte.
Phantom Types
Phantom Types (Tipos Fantasma) são tipos que nós criamos não com o objetivo de criar um objeto a partir deles, mas sim de armazenar informação extra utilizando o sistema de tipos.
Um pouco estranho, mas vai ficar mais claro com os exemplos.
Vamos começar criando dois phantom types bem simples:
public abstract class SIM { } public abstract class NAO { }
Eles são classes simplesmente porque C# não suporta abstract types.
A informação extra que queremos armazenar é a presença ou ausência do nome ou endereço.
public class Builder<TEM_NOME, TEM_ENDERECO> { public Builder<SIM, TEM_ENDERECO> ComNome(String s) { return new Builder<SIM, TEM_ENDERECO>(); } public Builder<TEM_NOME, SIM> ComEndereco(String s) { return new Builder<TEM_NOME, SIM>(); } }
Nota: Em geral, damos os nomes de T
, T1
, U
e outros nomes bem simples para os nossos tipos. Mas quando começamos a usar um pouco mais de type-level programming, se não dermos nomes bons para os tipos, fica tão complicado quanto programar com variáveis que se chamam x, y, b, k ou l.
Notem que agora no nosso Builder
possui dois parâmetros de tipo. O primeiro nos diz se o nome já foi preenchido e o segundo nos diz se o endereço foi preenchido. Com isso, fica bem simples agora disponibilizar-mos o método Build
apenas quando ambos estão preenchidos:
public static class Extension { public static Pessoa Build(this Builder<SIM, SIM> builder) { return new Pessoa(); } }
A única coisa que fica meio chata é que quando você instancia o builder, precisa lembrar de usar new Builder<NAO, NAO>();
. Para evitar isso podemos criar uma simples fábrica:
class Factory { public static Builder<NAO, NAO> NovoBuilder() { return new Builder<NAO, NAO>(); } }
Código final
Organizando um pouco esse código todo, temos:
namespace impl { public abstract class SIM { } public abstract class NAO { } public class Builder<TEM_NOME, TEM_ENDERECO> { public Builder<SIM, TEM_ENDERECO> ComNome(String s) { return new Builder<SIM, TEM_ENDERECO>(); } public Builder<TEM_NOME, SIM> ComEndereco(String s) { return new Builder<TEM_NOME, SIM>(); } } } namespace api { using impl; class Factory { public static Builder<NAO, NAO> NovoBuilder() { return new Builder<NAO, NAO>(); } } public static class Extension { public static Pessoa Build(this Builder<SIM, SIM> builder) { return new Pessoa(); } } } namespace user { using api; class FormaDe { public void uso() { var pessoa = Factory.NovoBuilder(); pessoa.Build(); // não compila pessoa.ComNome("").Build(); // Nem isso pessoa.ComEndereco("").Build(); // Ou isso pessoa.ComNome("").ComEndereco("").Build(); // compila pessoa.ComEndereco("").ComNome("").Build(); // compila } } }
O que vocês acharam dessa solução? E vocês podem ver essa mesma solução escrita em Scala neste gist.
Jonas Abreu