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