Monthly Archive for September, 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 conhece-lo, 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™.

A linguagem independente de linguagem

A palavra de hoje é modelagem.

Quando os programadores ouvem esta palavra, não pensam em porcelana, argila ou massa de modelar. Eles imaginam diagramas, esquemas, retângulos, setas e coisas do tipo. Bem estranho, mas é verdade.

O que os programadores chamam de modelagem não passa de uma forma de expressão que usa uma linguagem visual ao invés de uma escrita. Se os esquemas fossem quadros, o código-fonte seria um poema. São artes diferentes e têm públicos diferentes. Do mesmo modo que há gente que consegue apreciar melhor um poema do que um quadro, tem gente que entende melhor código-fonte do que esquemas.

Diferentemente do que acontece nas artes, os esquemas e diagramas dos programadores podem ser usados para gerar código-fonte sem nenhuma intervenção humana. Dependendo da tecnologia em uso, este código precisa ser lapidado manualmente para que possa ser finalmente executado por um computador. Pessoas diferentes conhecem linguagens de programação diferentes, por isso alguém teve a idéia de usar os diagramas para permitir liberdade na escolha das linguagens textuais.

Usar uma linguagem visual pode ser uma boa idéia, mas não para obter independência de linguagem. Esse objetivo de escrever (ou deveria dizer desenhar?) um programa de forma independente de linguagem é simplesmente inviável. Nunca chegaremos lá, simplesmente porque não dá para projetar nada sem uma linguagem, uma representação, específica. Se você vai escrever alguma coisa, precisa escolher uma linguagem qualquer e, por definição, não existe isso de linguagem independente de linguagem.

O que podemos fazer é desenvolver linguagens cada vez de mais alto nível que poderão ser traduzidas para diversas outras de baixo nível. Quando finalmente conseguirmos ficar livres das linguagens de base, vamos descobrir que estamos presos à mais abstrata. Ela pode ser mais produtiva, intuitiva e todo o resto que você queira imaginar, mas ainda é uma linguagem. Isto não é ruim.

O propósito de usar linguagens visuais não é poder trocar a linguagem de suporte. De preferência, não devia nem haver uma representação textual intermediária que precise de ajustes manuais. Na Terra dos Modeladores Felizes, diagramas são executáveis do mesmo modo que um conjunto de instruções de máquina. A verdadeira razão de haver linguagens visuais é que algumas pessoas acreditam que elas são mais abstratas e produtivas.

Abstração é causa direta da produtividade. A segunda não vem sem a primeira. Não adianta usar uma linguagem visual se ela possui exatamente as mesmas construções de alguma outra textual. Se ela puder ser traduzida para a forma textual com uma correspondência direta, não há ganho de produtividade. Na verdade, é bem provável que haja perda. O ponto é que programadores passam muito pouco tempo realmente digitando (ou desenhando). A parte mais difícil e trabalhosa é feita dentro da cabeça. Acelerar a edição é se concentrar no problema errado. O que queremos mesmo é maior expressividade. Isto pode muito bem vir de uma linguagem visual.

Ou não.

Afinal, não é qualquer imagem que vale mais do que mil palavras.

Erros de programação não existem…

…isso é coisa da sua imaginação.

Às vezes — e aqui sempre não seria uma aproximação tão grosseira — quem desenvolve um programa não sabe exatamente em que sistema ele será usado. É fácil ter uma ligeira idéia, mas há usuários demais para que seja possível ter certeza. Você já deve ter ouvido slogans do tipo “Escreva uma vez, rode em qualquer lugar”, mas isso é só propaganda. Claro que uma plataforma comum dá alguma segurança e aumenta a produtividade. Mas se você quer saber exatamente como seu programa se comporta nas mais variadas configurações, você precisa testá-lo. Não tem jeito. Você precisa soltar o coitado neste lugar inóspito que é o mundo real e ver como ele se sai. Pode parecer injusto, mas é para o bem dele.

Os usuários esperam que as coisas Simplesmente Funcionem ™ e saber o que eles esperam é um trabalho de requisitos e testes, não de programação. Isto não quer dizer que programadores não devam fazer isso, mas que as habilidades que eles devem ter vão além de escrever código. Isto envolve muito condicionamento, bastante experiência e um bocado de imaginação. Mas se um programa não funciona do modo esperado em alguma situação, ele não está errado. Se eu não programar o EclipseFP para saber que alguns módulos poderão ser importados de outra parte do mundo fora do meu controle, se seu formulário não estiver preparado para o espertinho que escreve os números por extenso ou se Joel Spolsky não previr que na Polônia o Alt-Direito é usado para digitar caracteres especiais, não quer dizer que nossos programas estejam incorretos. Eles estão corretos para algumas das possíveis situações.

Todo programa é escrito e testado com algum uso em mente. Mesmo que os testes não sejam escritos em linguagem de computador, todo mundo testa o que escreve pelo menos uma vez de forma manual. Obviamente estou simplificando um pouco as coisas aqui. Afinal existe gente que escreve programas e não precisa (ou não quer) se dar ao luxo (ou à sanidade) de executá-los antes da entrega. Mas, por favor, vamos desconsiderar esses prodígios (ou azarados) pelo bem do argumento. Se o código for executado e validado várias vezes de forma automática à medida que evolui, a chance de que deixe de funcionar diminui. Mas o fato é que, mesmo se você estiver escrevendo testes automáticos como deveria, eles não cobrem todas as situações. Eles não conseguem fazer isso. É simplesmente impossível porque o espaço de busca é potencialmente infinito.

Testes não podem garantir que um programa nunca vai se comportar de forma inesperada. Nada pode. Se seu programa for usado por uma variedade razoável de pessoas, alguém em algum lugar do mundo vai ter a chance de dizer “como diabos eles não pensaram nisso?”. Tudo que você pode fazer, se você tiver cuidado e reservar bastante tempo para pesquisa e testes, é garantir que quem vá dizer isso esteja em um lugar bastante remoto. Mas vai haver alguém. Sempre.

Erros de programação — diferentemente de fantasmas, lobisomens e sacis — não existem. O que existe mesmo são situações não previstas e testes que ainda não foram escritos.

O que é um programa?

Todo mundo é capaz de escrever um programa de computador. Até crianças de dez anos. Lembro que quando eu tinha essa idade, as aulas de informática na minha escola giravam em torno de uma tartaruga que desenhava algumas formas geométricas. Para isso, era preciso dar comandos a ela utilizando a linguagem de programação Logo (que pode ser experimentada utilizando o interpretador KTurtle, entre outros). Eu não posso dizer que gostava das aulas, mas o exemplo mostra que crianças também podem escrever programas.

Depois daquelas aulas de informática já escrevi alguns outros programas. Mas o que diabos é isto?

Esta página HTML que você está lendo é um programa? O editor que eu uso para escrevê-la (outra página HTML) é um programa? O navegador que você está usando para passear pela Internet é um programa? Um desenho gravado em formato bitmap é uma programa? E se o formato for vetorial?

Você já deve ter visto em algum shopping center aquelas máquinas que fazem desenhos bordados. Elas desenham a partir de padrões armazenados em meio eletrônico e há empresas especializadas em produzir estes padrões. Uma dessas empresas estava processando um de seus clientes por ter alugado cartuchos onde estavam armazenados os padrões e, recentemente, o tribunal considerou-os como dados e não como programas.

Pela lei deles, é permitida a troca de meios de armazenamento que contenham dados sob direitos autorais, desde que estes dados não sejam programas. Se alguém adquire legalmente dados e grava-os em algum meio físico (como um CD ou pen drive), ele tem permissão para vender, alugar ou doar este meio juntamente com os dados (mas não tem permissão para fazer outras cópias). Entretanto, se estes dados forem programas de computador, a transferência não pode ser feita. Portanto, se alguém entra em uma loja e compra um disco onde está gravado um programa, o disco a princípio é intransferível e não pode ser emprestado a mais ninguém.

A empresa da nossa história queria que seus padrões de bordado fossem considerados programas de computador. Assim o seu cliente estaria violando a lei ao ter alugado sua cópia para outros e muito provavelmente teria que pagar uma multa bem gorda.

Todo dado interpretado por computador é escrito em alguma linguagem projetada por alguém. Pode ser que o projetista da linguagem nem saiba que está projetando uma linguagem, mas nem por isso deixa de estar. HTML, por exemplo, é uma linguagem e seus projetistas sabiam disso. Sabiam tanto que colocaram este L no final que é justamente a inicial de Linguagem. O formato de imagem JPEG também é uma linguagem. Não é muito tratado como tal, mas também é uma linguagem. Há máquinas (muitas vezes simples programas de computador) capazes de ler e interpretar dados escritos nessas linguagens. Interpretar aqui pode ser mostrar uma saída para alguma pessoa. Exatamente o que está acontecendo agora para você ler estas palavras.

Um subconjunto bastante interessante de todas estas linguagens que a mente humana pode imaginar é o das linguagens Turing-completas. Se você cursou algum curso de computação e não matou nenhuma aula (o que, admito, é praticamente impossível para um ser humano), deve saber que estas são linguagens com poder computacional equivalente ao de uma máquina de Turing, um modelo teórico que pode ser usado para descrever até mesmo este computador de última geração que você tem aí. Qualquer linguagem de programação que se preze é Turing-completa. Na verdade, eu acho que uma linguagem precisa ser Turing-completa para ser chamada de linguagem de programação.

Para estas linguagens especiais, também é possível construir máquinas de interpretação e programas seriam simplesmente dados escritos em uma dessas linguagens. Interpretar aqui pode ser visto como traduzir o programa para uma outra linguagem que será novamente traduzida e que, no fim das contas, gera uma saída para alguém. Exatamente o que está acontecendo agora pra você ler estas palavras.

Mas e se os padrões de bordado fossem escritos em Ruby, Java ou Haskell? Eles seriam programas?

É perfeitamente possível desenvolver uma máquina de bordar que entendesse uma dessas linguagens. Seria mais fácil ainda construir uma máquina capaz de interpretar um subconjunto dessas linguagens. Para um programador humano, os padrões pareceriam claramente com o que chamamos de programa, mas para a máquina seriam diretamente equivalentes aos dados no formato inicial.

A diferença entre dados puros e programas não está nos dados, mas na máquina que os interpreta. O intrigante é que, do ponto de vista das máquinas (se é que máquinas têm ponto de vista), os programas não passam de dados.