Wednesday, September 27, 2006

Uma pequena grande idéia

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™.

3 Comments:

At 10/19/2006 02:49:00 PM, Anonymous Anonymous said...

A IDE para a "linguagem extremamente bela que quase ninguém usa" especificada no artigo já dá para conferir, pelo menos em protótipo: http://www.cin.ufpe.br/~haskell/vhs

[]s
-- AFurtado

 
At 10/19/2006 03:13:00 PM, Blogger Thiago Arrais said...

Já tentei muito rodar o VisualHaskell, para fazer meu estudo dos "concorrentes". Mas parece que eu preciso ter o Visual Studio antes para conseguir instalar o danado.

Existe alguma versão grátis do Visual Studio que eu possa baixar para poder testar o VHS? O instalador MSI (foi só o que eu achei para baixar na página) vem com o código-fonte? Mas o meu maior problema mesmo é que meu acesso a máquinas Windows é meio limitado. Por isso acho que vou ter que continuar fazendo minha própria IDE mesmo. Pelo menos eu aprendo um bocado no meio do caminho.

Por falar na linguagem extremamente bela, estive pensando em como as linguagens funcionais sem efeitos colaterais podem ser úteis nesses tempos de múltiplas CPUs e/ou processadores multi-núcleo. A última versão do GHC inclusive provê otimização para execução paralela em processadores SMP. Talvez Haskell deixe de ser "uma linguagem que ninguém usa", para ser apenas "extremamente bela". Afinal, as pessoas um dia vão começar a querer aproveitar suas múltiplas CPUs ao máximo. Acho que vou blogar sobre isso...

 
At 10/24/2006 05:36:00 PM, Anonymous Anonymous said...

O projeto até quando eu o deixei funcionava para versões 2003 do VS.NET, que infelizmente todas pagas. Talvez a evolução dele (http://www.haskell.org/visualhaskell/) possa ser usada em algum dos VS 2005 Express Editions...

[]s
-- AFurtado

 

Post a Comment

<< Home