Qual é o padrão de projeto que você mais usa? Que pequena estrutura insiste em aparecer quase que espontaneamente no seu código?
Eu particularmente nunca cheguei a contar quantas vezes uso cada padrão, mas tem um que com certeza estaria bem no topo da lista dos mais usados se eu contasse. Dica: não é o Singleton. Também não é nenhum dos padrões do clássico GoF. É um padrão bem pequeno e localizado que às vezes é chamado de Null Object. Muito provavelmente você já deve conhecer ele, seja por este ou outro nome. Isso de nomes diferentes para os mesmo padrões acontece com freqüência, já que muitos deles brotam em vários locais, várias cabeças, diferentes de forma independente.
Suponha que você esteja construindo uma wiki como parte de uma ferramenta de acompanhamento de projetos. Somente usuários autenticados podem editar páginas nesta wiki. Isso pode ser resolvido com algo parecido com isso (Ruby):
1 def edit
@page = find_page(params[:page_name])
if current_user.nil? ||
5 !page.editors.include?(current_user.login)
redirect_to :action => 'show',
:page_name = params[:page_name]
end
end
Mas aquela linha cinco está particularmente feia. Podemos esconder essa verificação num método bem nomeado na classe User.
1 class User
def can_edit?(page)
page.editors.include? self.login
end
5 end
Ainda há bastante espaço para melhoria aqui. Esse trem de chamadas de método da linha três está bem grande e pode ser diminuído, mas vamos primeiro ver como ficou a nova definição de edit
:
1 def edit
@page = find_page(params[:page_name])
if current_user.nil? ||
5 !current_user.can_edit?(@page)
redirect_to :action => 'show',
:page_name = params[:page_name]
end
end
Já melhorou bastante, mas aquela verificação de nulidade que ficou na linha quatro ainda está me incomodando. Há tantos detalhes embutidos nesta pequena chamada current_user.nil?
que podem fazer a cabeça de alguém explodir. Para entender isso é preciso saber que current_user
é igual a nil
quando o usuário não está autenticado. Além disso, a regra que diz que os usuários devem estar autenticados e autorizados a editar a página está toda codificada nesta pequena linha de código. É responsabilidade demais para uma única linha.
Eu não quero ser responsável pela explosão de nenhuma cabeça, portanto vou procurar algum modo de melhorar isto. Seria melhor se pudéssemos dizer somente current_user.can_edit?(@page)
, mas não podemos fazer isso porque o objeto nil
(sim, nil
, assim como qualquer coisa em Ruby, também é um objeto) que representa o usuário não autenticado não responde ao método can_edit?
. Por isso precisamos de todo esse tratamento desajeitado para o caso especial.
Mas o usuário não autenticado não precisa ser representado por nil
. Ao invés disso, podemos definir uma nova classe chamada UnidentifiedUser
que é um Null Object. Ela vai ser uma classe bem pequena, usada simplesmente para tratar o caso especial do usuário não autenticado. Os detalhes sobre tratamento de usuários sem autenticação vão ficar confinados a ela e o restante do sistema poderá tratar todos os usuários uniformemente. Segue o código:
1 class UnidentifiedUser
def can_edit?(page)
false
end
5 end
E acabou. O usuário não identificado só precisa responder que não pode editar nenhuma página, sem exceção. Como isto aqui é Ruby, não precisamos dizer que esta classe herda da User
original porque usamos duck typing. Se estivéssemos usando uma linguagem estaticamente tipada, precisaríamos disso (como vamos ver no próximo exemplo que usa Java).
Isso é tudo que precisamos para esta classe, mas ainda precisamos alterar o método current_user
, para que retorne um objeto UnidentifiedUser
ao invés de nil
no caso do usuário não estar autenticado.
1 def current_user
session[:user] || UnidentifiedUser.new
3 end
Outra forma de alcançar o mesmo efeito é injetar o método can_edit?
diretamente no objeto nil
, já que Ruby não diferencia tempo de compilação, linkagem e execução. Desse modo não precisamos nem modificar este último método.
Este pequeno padrão se mostra muito útil. Suponha agora que você esteja escrevendo um IDE para alguma linguagem extremamente bela que quase ninguém usa por achar diferente e esquisita. Este seu IDE usa um compilador externo por baixo dos panos e o programador pode escolher se quer ver a saída do compilador. Vamos começar pelo código feio e passar lentamente para uma versão mais elegante. Claro que isso é só minha opinião. Mas este é o meu blog, tudo aqui é só minha opinião. (Java)
1 class Compiler {
// ...
5 void compile(IFile target) {
if (compilerOutputEnabled())
output.write(
"Starting compilation from file "
+ target.getName());
10
String cmdLine = "ghc --make " +
target.getFullPath().toOSString();
if (compilerOutputEnabled())
15 output.write(cmdLine);
String output = processRunner.execute(
sourceFolderFor(target), cmdLine);
20
if (compilerOutputEnabled())
output.write(output);
}
25}
Eu avisei que iríamos começar pelo código feio... Você também está sentindo o mau cheiro? Essas linhas repetidas perguntando se alguma coisa deve ser relatada não estão te dando coceira? Vamos tratar isso antes que comecem a estourar bolhas.
O objeto output
apareceu magicamente para este nosso método. Estamos assumindo que ele foi inicializado pelo nosso Compiler
em algum ponto omitido do código. Mas não precisa ser assim. Podemos injetar qualquer Writer
se o output
for um parâmetro do método.
Vamos escrever então um Writer
especial que sabe quando deve escrever de verdade ou não e injeta-lo no método compile
por passagem de parâmetro.
1 public CompilerOutputWriter extends Writer {
// ....
5 @Override
public void write(char[] cbuf,
int off, int len)
{
if (compilerOutputEnabled()) {
underlyingWriter.write(
cbuf, off, len);
}
10 }
}
A responsabilidade de checar se a saída deve ser mostrada agora passou para o objeto CompilerOutputWriter
. Podemos tirar as checagens do método compile
:
1 class Compiler {
// ...
5 void compile(IFile target, Writer output) {
output.write(
"Starting compilation from file "
+ target.getName());
10 String cmdLine = "ghc --make " +
target.getFullPath().toOSString();
output.write(cmdLine);
15 String output = processRunner.execute(
sourceFolderFor(target), cmdLine);
output.write(output);
20 }
}
O código está ficando melhor, mas ainda dá pra deixá-lo mais apresentável. Podemos apagar a classe CompilerOutputWriter
por inteiro (você também tem uma ótima sensação quando apaga código?) e introduzir um NullObject que será selecionado quando a saída do compilador for desabilitada. Por exemplo:
1 public class NullWriter extends Writer {
// ....
5 @Override
public void write(char[] cbuf,
int off, int len)
{ /* ignore */ }
}
Um NullWriter
simplesmente ignora a entrada. O pobre compilador vai achar que está escrevendo alguma coisa, mas qualquer coisa que ele pedir será ignorada. Agora a responsabilidade de saber se o usuário quer ou não ver a saída do compilador é de quem está chamando o Compiler
, e ele tem todo o direito de delegar a responsabilidade.
O padrão NullObject é uma forma de tratar casos especiais que substitui a lógica condicional por uma solução mais limpa. Por falar nisso, parece que eliminar lógica condicional é uma Boa Idéia™.