Boa noite, tudo certo? :)
Acabei de passar por uma situação bastante interessante onde apliquei uma solução que eu achei bastante elegante. Gostaria de compartilhá-la aqui no Blog na esperança de que isso possa ajudar alguém. Vou utilizar uma exemplo hipotético, porém parecido com a situação real.
A situação atual
Um time está criando uma aplicação nova e decide escrever seu próprio mecanismo de logging.
Em um determinado momento foi criado um objeto de valor – que representa uma entrada de log – conforme a seguinte definição:
public class LogEntry
{
public LogLevel Level { get; set; }
public string Message { get; set; }
public DateTime DateTime { get; set; }
public string Url { get; set; }
}
public enum LogLevel
{
Warning,
Information,
Error
}
Alguém consegue achar um problema no código acima?
Teeeeempo!
1
2
3
…
10
Vamos imaginar que nosso framework de log cria uma instancia desta classe em um determinado local e depois repassa para uma classe de filtro conforme o código abaixo.
public class OnlyWarningFilter : IFilter
{
public bool ShouldLog(LogEntry entry)
{
return entry.Level.Equals(LogLevel.Warning);
}
}
Até ai tudo bem, este filtro vai fazer com que grave apenas os logs do tipo Warning. O problema é que estamos dando a oportunidade para esta classe simplesmente alterar completamente qualquer detalhe da LogEntry. Não só no filtro como também em qualquer local em que esta instância for repassada. Isto acontece pois definimos todas as nossas propriedades como públicas, tanto o get quanto o set. Olha só o que algum desenvolvedor pode fazer dentro do filtro.
public class OnlyWarningFilter : IFilter
{
public bool ShouldLog(LogEntry entry)
{
entry.Message = "All your base are belong to us!";
entry.DateTime = DateTime.Now.AddDays(-123);
return entry.Level.Equals(LogLevel.Warning);
}
}
Está instalado o chaos! Todos os nossos logs terão um conteúdo indesejado ao invés de uma mensagem informativa e confiável. Isto foi apenas um exemplo, é muito provável que ninguém faça isso, mas se pudéssemos prevenir seria melhor, não acha? A solução mais obvia é marcar o set das propriedades como private e criar um construtor público recebendo as quatro propriedades. Ficaria assim.
public class LogEntry
{
public LogLevel Level { get; private set; }
public string Message { get; private set; }
public DateTime DateTime { get; private set; }
public string Url { get; private set; }
public LogEntry(LogLevel level, string message, DateTime dateTime, string url)
{
this.Level = level;
this.Message = message;
this.DateTime = dateTime;
this.Url = url;
}
}
Problema resolvido. Se você se sente satisfeito por aqui, tudo bem, mas eu vou mais além!
Neste caso temos apenas quatro propriedades e o construtor ainda está bastante enxuto. Apesar de que, na minha opinião, quatro parâmetros no construtor já começa a ficar feio. Toda nova propriedade faria o construtor crescer mais e mais, o que é certamente algo indesejado.
A solução aplicada
Com certeza já ouviram falar que Design Pattern deve ser utilizado quando surge um problema já conhecido, este exemplo é a prova disto. Temos um problema e há um grupo de padrões exclusivos para criação de objetos. Vamos utilizar o Builder Pattern com algumas pitadas de Interfaces Fluente. O resultado é excelente. Para mais detalhes sobre estas duas técnicas, utilize os links acima, vou direto para a solução e explicação.
public class LogEntry
{
public LogLevel Level { get; private set; }
public string Message { get; private set; }
public DateTime DateTime { get; private set; }
public string Url { get; private set; }
private LogEntry(Builder builder)
{
this.Level = builder.Level;
this.Message = builder.Message;
this.DateTime= builder.DateTime;
this.Url = builder.Url;
}
public class Builder
{
public LogLevel Level { get; private set; }
public string Message { get; private set; }
public DateTime DateTime { get; private set; }
public string Url { get; private set; }
public Builder AsError()
{
return WithLevel(LogLevel.Error);
}
public Builder AsInformation()
{
return WithLevel(LogLevel.Information);
}
public Builder AsWarning()
{
return WithLevel(LogLevel.Warning);
}
public Builder WithLevel(LogLevel level)
{
this.Level = level;
return this;
}
public Builder MessageIs(string message)
{
this.Message = message;
return this;
}
public Builder CreatedOn(DateTime dateTime)
{
this.DateTime = dateTime;
return this;
}
public Builder OnUrl(string url)
{
this.Url = url;
return this;
}
public static implicit operator LogEntry(Builder builder)
{
return new LogEntry(builder);
}
}
}
A criação de uma nova entrada passou de:
LogEntry entry = new LogEntry(LogLevel.Error, "System is out of memory!", DateTime.Now, "http://localhost/ListOnlineUsers.aspx");
Para:
LogEntry entry = new LogEntry.Builder()
.AsError()
.MessageIs("System is out of memory!")
.CreatedOn(DateTime.Now)
.OnUrl("http://localhost/ListOnlineUsers.aspx");
A maior vantagem desta nova solução é a facilidade na leitura do código. No primeiro caso é mais difícil saber o que cada parâmetro significa, é necessário ver o nome de cara parâmetro no construtor da classe. Além disso, o primeiro exemplo está nos obrigando a atribuir as quatro propriedades, se uma delas fosse opcional, teríamos que escrever um novo construtor (aka Telescopic Constructor Pattern). Novamente a tendência é criarmos vários construtores conforme a necessidade. No segundo caso é possível atribuir apenas as propriedades que sejam necessárias.
Percebam também que temos a opção de definir o tipo de log utilizando os método auxiliares AsError(), AsWarning() e AsInformation() ou então, caso necessário, ainda podemos utilizar o WithLevel(LogLevel). Fica bastante agradável de ler e escrever, não acham?
Em Java seria necessário criar um método build() em nosso LogEntry.Builder que iria ser responsável por criar e devolver uma instância do LogEntry. No .Net é possível substituirmos este método por um operador de conversão implícita e evitar ter que explicitamente pedir para o builder construir o objeto.
Gostou? Espero que façam um bom uso :)
Não gostou de algo ou achou algum problema? Compartilhe!
Ps1.: Para quem nunca achou uma utilidade para as classes internas do .Net/Java, eis aqui um bom exemplo.
Ps2.: O código está no GitHub.
Ps3.: Nó GitHub eu dividi a classe em em dois arquivos utilizando o recurso de classes parciais, achei que ficou mais organizado.