Archive for the 'Desenvolvimento' Category

Sobre controle de versão distribuído

Um dos grandes desafios da vida de um profissional de tecnologia é de se manter atualizado em relação às novidades e rumos do mundo da computação. A quantidade de fontes de informação é “gigantescamente” enorme, e é necessário ter um bom filtro interno para conseguir separar o joio do trigo, extrair aquilo que nos interessa e manter a sanidade mental.

O meu processo de absorção de novidades é por etapas. Em geral eu leio ou ouço falar sobre algo novo, e guardo em um lugar remoto do meu cérebro. Muitas vezes, eu conto com a colaboração do meu colega de blog e junkreader profissional, senhor Lullis, Raphael Lullis. Com o passar do tempo, algumas das novidades vão sendo eliminadas, e algumas poucas são promovidas para a próxima fase. Após alguns paredões, eu acabo resolvendo aprender as que sobreviveram. Normalmente, as tecnologias que ficam são aquelas que por algum motivo se tornam necessárias no meu cotidiano.

Foi assim com django, com Ajax, com python. E agora, sistemas de controle de versão distribuídos (Mercurial ou Git) estão próximos da fase final de aprovação. Pra variar, o Raphael é parcialmente culpado por isso. Ele tem um lado Cuco: a cada 2 dias reaparece e martela sobre um tema. E vira e mexe ele vem me falar das maravilhas do uso de um Mercurial da vida. No começo achei um tanto quanto viajado. Mas agora, me parece extremamente útil.

Aliás, quero aproveitar antes para falar sobre o conceito de repositório. Pelo fato de um sistema distribuído não ter o conceito de repositório central, algumas pessoas acham que a coisa tende ao caos. Eu pensava assim. O grande lance é que ao contrário de um sistema como SVN onde existe um repositório e as pessoas baixam working copies, num sistema distribuído cada máquina é um repositório.

Dois elementos me parecem muito atraentes nestes sistemas, e me fazem falta atualmente: a possibilidade de ter um repositório local, e a possibilidade de compartilhar seu trabalho com outras pessoas sem a necessidade de passar por um repositório central.

O primeiro elemento é algo que pode ser parcialmente resolvido por bons IDEs como o Eclipse, que oferece um histórico local. Mas históricos locais são ótimos para resolver problemas em arquivos separados. A coisa se torna mais complicada quando queremos voltar conjuntos de arquivos. Para isso, já nos acostumamos a ter uma ferramenta de controle de versão, que permite comitar e comentar checkpoints de trabalhos. O fato é que quando se trabalha em grupo, o ideal é que os commits no repositório central sejam completos. Mas muitas vezes, uma implementação pode ser quebrada em várias sub-tarefas que não podem/devem ser compartilhadas com a equipe, e ter um histórico apurado destas subtarefas pode ser muito útil. Que jogue a primeira pedra aquele que nunca passou horas e horas escrevendo código,  e que no final das contas teve que voltar tudo atrás usando um revert.

O segundo elemento é algo que pode resolver vários problemas. Um deles é  permitir criar níveis de repositórios: um de desenvolvimento, um de testes, um de produção. Todos com histórico, tags, versões e afins. Trabalhei num projeto onde muitas vezes o repositório ficava instável, e era um deusnosacuda para montar uma versão para cliente.

Outra característica interessante é permitir que subgrupos dentro de uma equipe de desenvolvimento possam compartilhar código sem a necessidade de ter que criar um branch ou, pior, zipar o diretório e mandar por email. Tudo se resolveria com um merge simples entre pares.

Eu sei perfeitamente que para que tudo isso funcione, é necessário uma readaptação da equipe à nova ferramenta e sobretudo, ao novo modus operandi. Mas parto do pressuposto que as pessoas que trabalham comigo são espertas o suficiente para isso. E pensando bem, se fomos capazes de aprender a trabalhar com sistema de controle de versões, porque não seríamos capazes de aprender a trabalhar com um sistema distríbuido?

Micro introdução sobre orientação a aspectos (AspectJ)

Eu estava pensando em escrever sobre algumas coisas que têm me interessado ultimamente e que têm alguma relação com o post sobre vazamento de memoria em Java, mas eu percebi que não teria como, sem fazer uma pequena introdução sobre programação orientada a aspectos (aspect oriented programming, AOP). A gente já discutiu um pouco sobre isso no post sobre decorators em python, mas não fizemos nada mais introdutório (se você já tem uma noção do assunto, provavelmente não vai achar esse artigo muito útil). Então nesse post eu vou falar sobre alguns conceitos de desenvolvimento orientado a aspectos(AOSD), mas com ênfase descarada em AOP na forma como implementada em AspectJ. Eu não pretendo cobrir em profundidade o assunto, pretendo somente dar uma idéia da motivação e discutir alguns pontos sobre o paradigma. Além disso, eu vou usar na maioria a terminologia em inglês, pois eu acho estranho traduzir termos tecnológicos. Se alguem tiver interessado em termos em português, o pessoal do workshop em AOP (WASP’04) fez um catálogo de termos.

Motivação e Exemplo

No meu ponto de vista, AOSD não é algo que veio revolucionar o desenvolvimento de software, mas sim um avanço incremental do paradigma de orientação a objetos (OO). Todo bom programador sabe que se deve tentar distribuir as responsabilidades (concerns) do programa em módulos, de forma que cada módulo seja fácil de implementar, entender e reusar. Em C, por exemplo, funções podem ser consideradas módulos. Pra qualquer programa não trivial, você não vai criar somente uma função main(), vai separar a funcionalidade em muitas funções que se chamam pra realizar o objetivo do software. Já OO prega a modularização do software em uma hierarquia de classes e conta com polimorfismo, herança, etc. A motivação de AOSD é que existem algum concerns que entrecortam a hierarquia, ou seja, que mesmo criando essa hierarquia de classes, você não tem como separar dois concerns que deveriam ser separados (para serem melhor implementados, entendidos e reusados).

Pra não ficar somente no abstrato, vamos à um exemplo bem simples, que é utilizado constantemente pra apresentar AOP. Imagine que você tem uma classe que faz o controle do acesso ao banco de dados. O código abaixo é um esboço de como uma classe dessas seria implementada em Java. Você tem um gerente de transações e, em todos os métodos de acesso ao banco de dados, você chama o gerente pra iniciar uma transação e fazer commit se tudo deu certo, ou rollback se deu algum problema. Note que hoje em dia ninguém faria um código assim, já que existem soluções melhores do que ficar escrevendo SQL no seu código (como Hibernate), mas é bem ilustrativo do problema.


public class DB {
	TransactionManager manager = ...;

	public void storeUser(User u) {
		manager.start();
		try {
			// write some sql code storing u into the DB
			manager.commit();
		} catch (DBException e) {
			manager.rollback();
		}
	}
	public void storeAccount(Account a) {
		manager.start();
		try {
			// write some sql code storing a into the DB
			manager.commit();
		} catch (DBException e) {
			manager.rollback();
		}
	}
	...
}

Claramente dá pra perceber que eu tô misturando dois concerns na mesma classe. Um é persistência (salvar o User e a Account no banco de dados) e outro é controle de transações. Em OO, não tem um jeito elegante de separar esses concerns. Você pode até tentar fazer uma interface com uma callback pra tentar minimizar a repetição de código, como no código abaixo, mas não fica ideal.


public interface TransactionalTask {
	public void execute();
}
public class TransactionManager {
	TransactionManager manager = ...;

	public void executeInTransaction(TransactionalTask task) {
		manager.start();
		try {
			task.execute();
			manager.commit();
		} catch (DBException e) {
			manager.rollback();
		}
	}
	...
}
public class DB {
	public void storeUser(User u) {
		manager.executeInTransaction(new TransactionalTask() {
			public void execute() {
				// write some sql code storing u into the DB
			}
		});
	}
	...
}

A idéia de AOSD é criar um novo módulo (chamado aspecto) em que você possa encapsular esses concerns que não tem como você modularizar com classes. AspectJ é a implementação mais conhecida de AOP, tanto por ser a primeira, quanto por ser bem robusta e contar com apoio de diversas empresas. Em AspectJ, que é uma extensão de Java (todo código Java é código AspectJ válido), nós criaríamos um aspecto pra encapsular controle de transações. O pseudo código abaixo mostra a idéia (eu não testei o código, então não garanto copy-and-paste funcionando).


public aspect TransactionManagerAspect {
	TransactionManager manager = ...;

	around(Object o) : execution(public void DB.*(..)) && args(o) {
		manager.start();
		try {
			proceed(o);
			manager.commit();
		} catch (DBException e) {
			manager.rollback();
		}
	}
}

public class DB {

	public void storeUser(User u) {
		// write some sql code storing u into the DB
	}
	public void storeAccount(Account a) {
		// write some sql code storing a into the DB
	}
	...
}

A classe DB agora contém somente código relativo à persistência e o aspecto é responsável pelo controle de transações. Em terminologia de AOSD, around é um tipo de advice e execution um tipo de pointcut. O que esse código tá dizendo é que sempre que a execução do programa for iniciar um método publico, que retorna void, na classe DB, o método deve ser substituído (por isso “around”) por esse advice aqui. O código do advice vai fazer o controle da transação e vai chamar proceed pra executar o código original. Note que o código do advice é muito parecido com a nossa solução usando callbacks. A grande novidade aqui é que usando pointcuts a gente consegue especificar quando que a “callback” deve ser usada, deixando o código da classe DB bem mais limpo.

Esse exemplo mostra a parte dinâmica da história, isto é, como usar aspectos pra “interceptar” alguns pontos da execução de um programa e inserir (ou remover) funcionalidade (código) nesses pontos. Mas AOSD também trata da parte estática ou “estrutural” da coisa. Por exemplo, imagine que no nosso exemplo nós quiséssemos inserir um controle de concorrência bem primitivo, pra garantir que ninguém modificou o User no tempo entre eu buscar no banco e salvar. Pra isso, eu poderia adicionar um campo na classe User com um contador que é incrementado sempre que ele for salvo no banco. Desta forma, na hora de salvar, eu confiro se o valor do contador no objeto que recebi é o mesmo do banco de dados. Se não for, significa que alguém alterou o objeto e eu recebi um objeto inconsistente. Novamente, o problema é que esse campo não faz parte do concern persistência ou modelo de dados. Portanto, não faria sentido eu declarar o contador na classe User. Pra resolver esse problema, AspectJ usa intertype declarations, que permitem a declaracão de campos e métodos em outras classes. Por exemplo, o código abaixo declara e inicializa um campo counter na classe User, removendo dela o concern tratamento de concorrência.


public aspect ConcurrencyManagerAspect {
	long User.counter = 0;
	...
}

Conceitos

Quando eu estava descrevendo o exemplo eu me referi à alguns conceitos de AOSD. Mas acho importante deixar um pouco mais explícito, então seguem algumas definições.

Existem alguns eventos que acontecem durante a execução de um programa que são interessantes de serem interceptados. Esses pontos são chamados join points. Nós vimos que a execução de um método é um ponto interessante quando o objetivo é substituir o método ou introduzir funcionalidade antes ou depois do método. Outros join points, por exemplo, são a chamada de um método (não confundir com a execução), a leitura ou escrita de um campo, ou a inicialização estática de uma classe. Mas esses join points são eventos concretos que acontecem em tempo de execução. Pointcuts são um jeito de especificar de forma abstrata (por exemplo, com regular expressions) quais join points você quer interceptar. AspectJ oferece pointcuts primitivos pros join points mais importantes (como execution, call, get, etc) e outros pointcuts usados pra identificar os objetos envolvidos nos eventos e ligá-los a variáveis. No nosso exemplo, args foi usado pra pegar uma referência ao objeto passado como parâmetro ao método. Outros pointcuts importantes são target (e.g., o objeto que é o destino de uma chamada de método) e this (e.g., o objeto que faz a chamada).

Quando um join point acontece em runtime e ele faz match com um pointcut, então se executa o advice relativo ao pointcut (se tiver algum). Nós vimos como um advice do tipo around funciona. AspectJ também tem advices before e after (que pode ser after throwing ou after returning), que servem pra introduzir código antes ou depois do join point. Intertype declarations também são disponibilizadas por AspectJ pra introduzir novos campos métodos em outras classes, como vimos no exemplo.

Um aspecto é então um módulo usado pra encapsular todas essas construções, do mesmo jeito que uma classe encapsula métodos e campos. O objetivo principal de um aspecto é de modularizar um interesse (concern) que não faz parte da decomposição normal da hierarquia de classes. Esses concerns são chamados de crosscutting concerns, porque eles entrecortam a hierarquia. A implementação de um concern é dita espalhada (scattered) quando ela não está localizada, e entrelaçada (tangled) quando ela está misturada com outros concerns.

Pra quem tiver interesse, o AspectJ Programming Guide é um excelente tutorial com bons exemplos e referências pros diversos tipos de pointcuts, além de explicar alguns problemas que você pode encontrar quando não tiver muita experiência com aspectos.

Discussão

No texto acima eu tratei AOSD e AOP quase como sinônimos, mas eu quero enfatizar que são distintos. Orientação a aspectos surgiu como AOP, pois trazia soluções pra problemas diretamente ligados ao código, como no exemplo sobre controle de transações. Contudo, percebeu-se que seria interessante elevar o nível de abstração e que crosscutting concerns podiam também ser encontrados em design, arquitetura, análise de requisitos, etc. Então AOSD trata não só da programação, mas também de todo o conjunto de metodologias, processos, etc, ligados ao desenvolvimento de software usando a idéia de que crosscutting concerns devem ser tratados como “first class citizens”.

Na minha experiência, AOP como encontrada em AspectJ é utilizada quase que exclusivamente em círculos acadêmicos. Existem algumas empresas que adotam AspectJ para alguns projetos, mas a linguagem está longe de ser adotada em larga escala. Porém, alguns conceitos de AOP são muito usados em alguns frameworks e application servers como Spring e JBoss. Spring, por exemplo, tem o conceito de interceptors, que nada mais são do que pointcuts e advice, mas menos poderosos (não permitem interceptar join points de pouca granularidade, como getters, se não me engano).

Eu acredito que existem atualmente dois grandes impedimentos pra se usar AOP em larga escala. Primeiro é a tecnologia do compilador. Hoje em dia ninguém aceita trabalhar com um compilador que não seja incremental. Quando você usa Eclipse e faz uma alteração no código, o JDT só compila o necessário. O similar para AspectJ, AJDT, tenta fazer o mesmo. Porém, a tarefa é muito mais difícil, pois uma pequena mudança tem que ser verificada perante todos os pointcuts do seu sistema, pois de repente o que você mudou pode agora ser interceptado pelo pointcut. Assim, apesar de ter uma performance relativamente boa, trabalhar com AspectJ usando AJDT não é tao agradável quanto trabalhar com Java usando JDT.

Segundo, AOSD não vai acabar com todos os seus problemas. É difícil pros programadores entenderem os conceitos e leva tempo pra comunidade desenvolver design patterns e entender o que tirar de melhor do paradigma. Pointcuts são atualmente regular expressions, então eles são frágeis e qualquer mudança de nome de um método pode fazer um pointcut não mais interceptar aquele método, e você não tem como perceber que isso aconteceu. Suporte de boas ferramentas é então essencial. Além disso, eu tenho dúvidas de que aspectos sejam bons pra modularizar concerns heterogêneos como “logging”, que foi sempre um dos principais exemplos. O problema é que cada instância de logging é muito específica pro contexto, então não faz sentido você remover todas as chamadas de log e encapsular em um advice, pois o advice teria que ser diferente pra cada ponto que ele interceptasse. Controle de transações é homogêneo, pois o advice sempre faz a mesma coisa, então é um exemplo mais apropriado. Existem estudos que apontam que Mixin Layers são melhores pra concerns heterogêneos como logging, mas isso seria assunto para um outro post.

==========================
Nota no dia 5/11

Conversando com o Miguel eu acabei percebendo algo que talvez não tenha ficado muito claro. AspectJ é uma extensão de Java e precisa de um compilador especial que compila as classes como se fossem Java e depois analisa os aspectos do sistema e faz diretamente as modificações no código (ou introduz testes de tempo de execução quando não tem como verificar estaticamente o resultado). Esse último passo, “introduzir os aspectos”, é chamado weaving.

Existem duas alternativas principais de compiladores pra AspectJ. O ajc é parte do AJDT, que é uma extensão do JDT do Eclipse. Como o JDT, o AJDT contém editor, compilador, debugger, e tudo o mais, implementados em plugins pro Eclipse. O principal objetivo do projeto é que a compilação seja rápida pra que você tenha a mesma experiencia que programando em Java, ou seja, ele tenta ser incremental e o código do compilador em si é otimizado (leia-se, não-facilmente extensível).

A outra alternativa é o abc (Aspect Bench Compiler), que é um compilador acadêmico cujo objetivo principal é ser extensível pra que se possa fazer pesquisa com linguagens de programação, otimização de código, análise estática, etc. Dessa forma, compilar um sistema grande com abc, principalmente usando as otimizações disponíveis, é bem demorado. O ideal é programar usando o AJDT e usar o abc pra gerar o bytecode de produção, já que ele é bem mais otimizado que o gerado pelo ajc.

Além disso, AspectJ não é a única linguagem orientada à aspectos derivada de Java. No site da aosd.net, por exemplo, tem uma lista com alguns sistemas (derivados ou não de Java) e algumas ferramentas orientadas a aspectos.

Vazamento de memória em Java

Isso existe?!

Sim….

Uma das grandes coisas que Java trouxe para o mundo do desenvolvimento foi uma moderna JVM, e um Garbage Colector eficiente, que foi se aprimorando ao longo dos anos. Com isto, o desenvolvedor pode deixar de se preocupar com questões de alocação e liberação de memória, fontes de perdas de cabelos e noites de sono perdidas de muitos programadores C/C++.

Nestas linguagens, um vazamento de memória se caracteriza como sendo uma região do heap alocada pelo desenvolvedor em algum momento do ciclo de vida de um software, cujo ponteiro se perde. Assim sendo, fica impossível (ou pelo menos extremamente difícil) recuperar esta área de memória para uso futuro. O efeito imediato é que o consumo de memória vai aumentando com o tempo, até tornar o sistema inviável.

É fato que este tipo de situação não existe em Java, uma vez que em última instância, a JVM sempre terá uma referência para uma área de memória alocada pelo desenvolvedor, e cuidará de fazer a limpeza caso necessário. Mágica? Não, tecnologia. Explicando de forma beeeeeeeeeeem resumida, o GC mantém uma contagem de quantas referências criadas pelo desenvolvedor apontam para um determinado objeto. Quando este contador chega a zero, está na hora de limpar a memória, pois com certeza esta não será mais útil.

Fica claro então que áreas de memória nunca serão perdidas em Java.

Miguel, você bebeu? Como é que vazamentos podem ocorrer então?

Bom, quem nunca perde a referência é a JVM.  Mas é possível perder o controle das referências para um objeto. Como eu disse antes, o GC coleta objetos sem referência. Mas e se você esquecer que tem referências perdidas no código e não zerá-las? Aí o objeto nunca será removido, e podemos ter um vazamento de memória, segundo Java. Para apaziguar certos ânimos, passarei a chamar este problema de Travamento de memória.

A boa notícia é que é bem mais fácil consertar em Java, e isto só se torna um problema real em grandes projetos que criam muitos objetos ao longo do tempo. Se você desenvolve exclusivamente para Web usando servidores de aplicação como JBoss, provavelmente pode fechar este artigo e voltar às suas atividades cotidianas.

A má notícia é que como se criou o mito de que este tipo de situação é impossível, ninguém hoje em dia se preocupa mais com isso. Não que isto seja ruim por si só. O problema é que quando suspeitamos de que isso pode estar acontecendo, consertar pode ficar mais complexo.

Uma situação clássica que pode gerar travamentos é o uso de Observers/Observables. Suponha que você tenha um objeto A do tipo Observable, e que ao longo da vida do seu software, você crie constantemente novas intâncias de objetos do tipo B que se cadastram com observadores de A. Nesta arquitetura, A contém referências a todos as intâncias de B, de forma não explícita (ou seja, contém referências à interface Observer). Basta alguém esquecer de limpar a referência da lista de observers e pronto…memória travada!

Como resolver isso? Bem, tentar racionalizar o processo de criação e limpeza de objetos, centralizando estes processos em pontos bem controlados. O processo de “limpeza” de recursos é muito importante, sobretudo quando a árvore de objetos e referências for grande.

==========================

Nota do dia 26/10

Como bem lembrado pelo leitor Thiago, uma outra possível solução é o uso de WeakReferences em Java. Um objeto deste tipo funciona como uma referência normal em Java, mas não trava o GC: caso um objeto seja referenciado apenas por WeakReferences, ele será coletado normalmente. Vale a pena olhar este artigo:

http://www.ibm.com/developerworks/java/library/j-jtp11225/index.html

Rapidinha

Só pra falar algo que talvez já seja óbvio pro pessoal mais esperto: o trio Python/Django/Prototype (Scriptaculous) tem um poder incrível de ajudar a desenvolver coisas úteis, eficientes e bonitas em pouquíssimo tempo. A minha produtividade com esta trinca tem sido muito grande, fator essencial para a evolução dos meus projetos pessoais (job4dev e sigaseutime).

Citação do dia

Lida no Manifesto 37 Signals, versão original (que eu nunca tinha lido)

My Cousin’s Buddy is a Web Designer

Then let him do it.

The money you save by using your cousin’s buddy is nothing compared with the cost in time and money required to undo his mistakes.

Simples. Perfeito.

Bizarro… mas muito útil!

O artigo do Miguel sobre perólas do mundo Java me fez lembrar de um trecho de código com o qual me deparei há um tempo atrás que me parecia também muito bizarro… até eu sacar um pouco depois a imensa utilidade dele!

O código, em linguagem C, era algo como o seguinte:

do {
  __alguma_coisa;
} while (false);

Em princípio fazer uma construção de laço para executar algo apenas uma vez parece algo bem bizarro, não?! Diria que é quase um primo-irmão do if (liberado==true). Foi exatamente o que eu achei. Mas colocando esta construção dentro do contexto em que eu a achei eu consegui ver (depois de alguma pesquisa, é claro) que ela é extremamente útil em alguns casos.

A primeira vez que eu me deparei com esta construção eu estava analisando o código-fonte do GNUChess, um jogo de xadrez de código livre. Como eu sei que o pessoal que escreve código livre algumas vezes utiliza umas construções bem diferentes do que normalmente estamos acostumados, resolvi investigar o porquê deles estarem utilizando esta construção. Afinal de contas, se este código é livre e várias pessoas já devem tê-lo revisado, não havia motivo para um laço aparentemente tão inútil estar lá sem alguma utilidade verdadeira, não é mesmo?!

E foi só pesquisar um pouco sobre o assunto para enteder melhor o que estava acontecendo. As construções como a acima só apareciam em definições de macros. Nunca em código “normal”. Para quem talvez não tenha muita familiaridade com macros em C, elas são basicamente códigos que você define e que serão literalmente expandidos pelo compilador onde seu identificador aparecer no código. Ou seja, um código assim:

#define macro_exemplo printf("Macro de exemplo!\n");
int main(void) {
  macro_exemplo
  return 0;
}

seria entendido pelo compilador, após o processamento das macros, da seguinte forma:

int main(void) {
  printf("Macro de exemplo!\n");
  return 0;
}

Ou seja, o preprocessador de código do compilador substitui a referência à macro_exemplo pela sua definição literal. No caso do nosso código bizarro, teríamos que definir a macro da seguinte forma:

#define macro_exemplo2 do { __alguma_coisa; } while (false);

ou ainda:

#define macro_exemplo3 do { \
                         __alguma_coisa; \
                       } while (false);

Note que uma macro deve ser definida sem quebras de linhas ou com quebras de linhas sinalizadas pelo caracter de escape barra invertida (’\').

Mas, ainda assim, não haveria motivo para o laço na minha macro, certo? Bom, se você pensar na macro como um código por si só, talvez isto esteja certo, mas não podemos esquecer que este código será copiado literalmente para algum outro lugar sempre que o identificador da macro for referenciado pelo programa. E muitas vezes não queremos fazer macros simples como as que eu mostrei no meu exemplo. Por exemplo, podemos querer fazer algo mais complexo como o seguinte:

#define IMPRIME_ESPACOS(n) for (int j = 0; j < n; j++) printf(" ");
#define ROTULO(rot) printf("L%d:", rot); do { \
          int n = numeroDigitos(rot); \
          IMPRIME_ESPACOS(6 - (n + 2))\
} while (false);

A macro IMPRIME_ESPACOS(n) imprime uma quantidade de espaços em branco representadas pelo “argumento” n. Estou usando argumento entre aspas porque o argumento de uma macro em C se comporta de maneira diferente de um argumento de função, já que não há checagem de tipo e sim uma simples substituição do valor passado como argumento da macro nas suas referências dentro da própria macro. Já a macro ROTULO(rot) recebe como “argumento” um rótulo e imprime Lrot seguido de um numero de espaços de tal forma que todas as linhas que chamarem a macro ROTULO(rot) estarão identadas na mesma posição (desde que o rótulo tenha menos de 6 caracteres, que era o limite utilizado no programa a partir do qual tirei o código de exemplo).

Alguém certamente já bateu o olho no código acima e deve ter dito: poxa, eu poderia chamar a função
numeroDigitos(rot) dentro de IMPRIME_ESPACOS na definição da macro ROTULO. Não podemos nos esquecer no entanto que macros não são chamadas de função e que onde se lê IMPRIME_ESPACOS em ROTULO dever-se-ia ler na verdade a definição de IMPRIME_ESPACOS. Neste caso, se passássemos uma chamada de numeroDigitos como “argumento” de IMPRIME_ESPACOS, a função numeroDigitos seria chamada dentro do loop definido em IMPRIME_ESPACOS sempre que a condição de parada do loop fosse ser calculada. Ou seja, teríamos um número excessivo e desnecessário de chamadas a numeroDigitos. Para solucionar este eventual problema, decidi criar uma variavel para armazenar o valor da chamada de numeroDigitos e passar esta variavel como argumento para IMPRIME_ESPACOS. O problema é que dependendo de onde este código for ser substituído pode não ser permitido ter declarações de variáveis.

Neste último caso (e em muitos outros), o código mostrado no início deste post, por mais bizarro que possa parecer a primeira vista, pode ser a solução, pois o uso do do { ... } while (false); torna possível a criação de um contexto local dentro das chaves do falso loop na macro. Dentro deste contexto é possível criar-se quantas variáveis se queira ou, por exemplo, encadear comandos dentro de uma macro que não ficarão perdidos se esta macro for utilizada dentro de um if-else.

Ah, e se alguém ficou curioso, procure na Internet: vocês verão muitos outros usos do do-while (false). Sinceramente, muitos deles, na minha opinião são dispensáveis e só servem para complicar código que poderia ser escrito de outra forma. E, pra falar a verdade, a única construção que eu já achei em códigos livres foi esta que eu cito aqui, dentro de macros. É preciso ter dicernimento na hora de usar este tipo de construção. Aliás, para falar a verdade, mesmo este caso que eu descrevi aqui, que eu acho interessante e útil eu só usária em últimos casos em códigos profissionais.

Meu Quadro de Medalhas: Aprendendo Ruby

O Soro escreveu há um tempo atras sobre como conseguir motivação pra aprender algo novo, como uma linguagem de programação. Desde a primeira vez em que ouvi falar de Ruby, la em 2001, eu estou tentando achar algum jeito de aprender a linguagem, mas a procrastinação sempre fala mais alto. Acho que o único jeito que encontrei pra aprender uma nova linguagem é ter um projeto, alguma coisa prática de verdade, nem que seja algo meio sem sentido mesmo, só pra ter um objetivo concreto e ir aprendendo com as necessidades que vão surgindo. Como não tava tão inspirado pra criar um novo job4dev (que foi o “projetinho” do miguel), e estamos em tempos de Olimpíadas, eu resolvi aprender um pouco de Ruby escrevendo um programa pra gerar meus próprios quadros de medalhas.

Eu sempre tive uma certa ressalva com esses quadros de medalhas olímpicos. Em geral eles colocam medalhas de ouro acima das de prata, e essas acima das de bronze. Isso faz com que um país que tenha uma mísera medalha de ouro fique na frente de um outro que tenha 10 de prata, o que eu acho meio que injusto. É claro que agora com os EUA perdendo da China nos ouros, a TV americana tá colocando o ranking baseado no total de medalhas. Mas daí você tá dizendo que uma medalha de prata vale o mesmo que uma de ouro, o que não é a verdade. Uma solução seria colocar pesos pra cada tipo de medalha.

Outra coisa meio injusta é que na China tem muito mais gente que na Austrália, então um ranking baseado em população também seria interessante. E mais ainda, um atleta como o Michal Phelps consegue 8 medalhas numa olimpíada, mas um time completo de futebol consegue no máximo 1. Porque não multiplicar o valor da medalha pelo numero de atletas na equipe (titular, pelo menos)?

Então a idéia inicial que eu tive foi a seguinte. Estamos em plena Web2.0, então eu devo conseguir alguns serviços que provenham os dados necessários e eu faco uma espécie de mash-up, calculando meus rankings. Eu precisaria de 2 serviços, um com os dados sobre as medalhas e esportes, e outro com dados sobre as populações dos países.

Infelizmente logo na minha primeira tarefa eu já encontrei problemas. Informação é dinheiro, e foi impossível encontrar algum site na Web que disponibilizasse as informações sobre as medalhas. Existem vários sites mostrando tabelas ou disponibilizando RSS com noticias, mas uma API pras medalhas não existe. Acabei descobrindo que existe uma tal de World News Press Association que tem o monopólio das feeds sobre os eventos olímpicos e eu teria que pagar pra conseguir acesso (como o google fez).

A minha solução foi criar um programinha em Ruby que fizesse parser das paginas do UOL. Primeiro eu baixo a pagina do quadro geral de medalhas, daí eu recursivamente baixo as paginas de cada pais (que tem no combo box). Não é exatamente uma solução elegante, mas graças as boas tags no fonte da UOL e ao HPricot (uma biblioteca pra fazer parse de HTML) não foi tao difícil agregar os dados e gerar um CSV. Porem, surgiram dois problemas. Primeiro que eu tive que abandonar minha idéia de multiplicar as medalhas pelo numero de atletas no esporte, pois essa informação seria muito chata de conseguir. Depois que eu tive que criar uma tabela traduzindo os nomes dos países de português pra inglês, já que eu queria apresentar as tabelas em inglês, e os dados de população também estariam em inglês.

O segundo passo seria conseguir informações sobre população. Eu achei um tal de Population Reference Bureau com as informações necessárias, só que de novo esbarrei em problemas. Primeiro que eles não tem um RSS ou um web service, então eu tive que fazer copy e paste da tabela. E segundo que algumas informações relativas aos países são diferentes da pagina da UOL, ou tem que ser agregadas (por exemplo, tive que adicionar a população de varias colonias francesas na da França). Pra alguns países estranhos (como “Ilhas Virgens Americanas”), que não tinham informação no Bureau, eu tive que pegar no wikipedia mesmo.

Finalmente, com todos os dados em arquivos, eu pude criar um outro programinha em Ruby pra analisar os dados e gerar meus rankings. Eh claro que como isso é just for fun, a interface deixa muito a desejar. Mas pra quem tiver interesse, alguns rankings que eu gerei estão nesse diretório. Eu atualizo os dados usando o parser uma vez por dia. Os cabeçalhos dos rankings descrevem o que foi usado pra gerar os”values” pras medalhas (counters) e pro valor total (aggregators) a partir do numero de medalhas, e por quais campos ordenar os países.

Bom, lições aprendidas:

  • realmente o melhor jeito pra aprender uma linguagem de programação é usando. Eu não posso dizer que agora sou um programador Ruby, mas aprendi um monte de detalhes e foi bem melhor do que qualquer tutorial.
  • apesar de existir muita informação pela web, o mais chato (e difícil) é conseguir agregar a informação de varias fontes que em geral são “um pouquinho diferentes”. Por exemplo, não teria como automatizar o matching dos nomes dos países em diferentes línguas, nem como programar a intuição de “somar as populações das colonias”, a não ser que você tenha muito mais meta-informação, ou recorra a heurísticas.
  • a China nem é tao boa assim se a gente considerar o tamanho da população ;-)
  • o Brasil continua ruim, em qualquer ranking mais ou menos razoável que se faça.

O proximo passo seria aprender Ruby on Rails e fazer uma interface em que os usuarios pudessem escolher como fazer os rankings. Mas como eu teria que instalar outro servidor web e um banco de dados, e eu to meio sem tempo, vai ter que ficar pra depois… e se interessar a alguem eu mando os dados.

Bomba do leitor

Plagiando a sessão da revista Mac+ mostro aqui um “bug” enviado por Roberto Endo (também conhecido como o rendo). O bug não é de um produto da Apple, mas sim do Google.

bug_google.gif

Os usuários assíduos dos produtos do Google reconhecerão na imagem acima um pedaço da tela de configuração. O interessante é que uma dada sequência de comandos (que o Roberto não revelou) faz aparecer a linha de javascript na barra inferior do browser. Não sei vocês, mas eu achei no mínimo curioso o if(0 != 1)….

Pérolas do mundo Java

Pérola 1

Essa é na verdade um par de pérolas.

Boolean.FALSE.toString ().toUpperCase ().equals ( meuObjeto.isControlado() )

Quando vi o código acima, já achei ridículo o fato do programador usar a expressão Boolean.FALSE.toString().toUpperCase() apenas para gerar uma string “FALSE”. Poderia ter sido mais simples escrever

"FALSE".equals(meuObjeto.isControlado())

Provavelmente o desenvolvedor queria se prevenir. Vai que false resolve mudar de nome no meio do caminho! E daí eu me perguntei: porque ele está testando string? Daí encontrei a seguinte função:

 public String isControlado () {
    if ( this.getElementos () != null ) {
        if ( this.getElementos ().size () > 0 ) {
            return "TRUE";
         }
        return "FALSE";
    }
    return "FALSE";
}

Perfeito. Porque usar lógica booleana?

Pérola 2

public List procuraPorExemplo(int arg0, boolean arg1){
    return super.procuraPorExemplo(arg0, arg1);
}

Pérola 3: sem comentários

public static boolean verificaAMaiorB(int a, int b) {
        boolean resultado = false;
        if (a > b) {
            resultado = true;
        }
        return resultado;
    }

Pérola 3

if ( objeto != null ) {
    lista = procura( objeto );// XXX
} else {
    lista = procura ( null );
}

Pérola 4

if ( comandoEscala.equals ( "alterar" ) == Boolean.FALSE ){
   ....
}

Rapidinhas sobre Python

A Sun criou um espaço dedicado ao Python dentro do Sun Developer Network.  Na página, existem links para o Dive Into Python, dicas para montar um ambiente com Django, Jython ou Python, GlassFish, etc. Vale ressaltar também que o NetBeans começou a dar suporte oficial para o Python.

A Apple começou a dar suporte para Python há algun tempo atras, permitindo que desenvolvedores criassem programas em  Python com interfaces gráficas nativas em Cocoa usando o Interface Builder e o XCode.

A impressão que eu tenho é que nos ultimos 12 meses, Python tem consistentemente ganhado espaço em blogs, sites especializados, projetos web. O futuro parece promissor.

Coisas bizarras de Python

Venho trabalhado nos meus projetos pessoais com Python há um certo tempo e estou bem satisfeito. Esta linguagem tem sido um fator importantíssimo na minha produtividade, e tem me permitido por minhas idéias no ar de forma estruturada e com qualidade em tempos recordes.

Mas acho importante identificar pontos falhos que possam ser melhorados. Ter uma visão crítica de uma linguagem não a desmerece e não me faz ser menos entusiasta. Foi assim com Java, é assim com Python.

Até agora, a única caracteristica de Python que realmente me incomodava era o fato de variáveis locais não terem escopo de bloco. Ou seja: uma variável criada dentro de um if existe após ele. Muitos consideram isso como um ponto que pode gerar vários erros bestas (e de fato pode), mas se bem utilizado, pode ser bem poderoso. Resumindo: requer cuidado, mas pode ser útil. Ninguém é perfeito…

Mas existem alguns limites…e hoje descobri uma característica que me incomoda. Não a ponto de eu deixar a linguagem, porque ainda acho que os benefícios dela são gigantescos. Mas incomoda. E fico feliz de ter descoberto isso antes de ter passado horas e horas quebrando a cabeça com comportamentos bizarros.

Chega de preâmbulos, vamos ao ponto:

def f(item, lista=[]):

     lista.append(item)

     print lista

Se executarmos a a função uma primeira vez, passando apenas um argumento, o resultado é o esperado: f(1) imprime [1] na tela. Mas se executarmos uma segunda vez, em vez de imprimir [2], a função f imprime [1,2]. Eu custei a acreditar nisso, e tive que confirmar pessoalmente.

Fazendo uma busca no Google, descobri na documentação oficial (http://docs.python.org/ref/function.html) que isto realmente é uma característica mapeada da linguagem:

Default parameter values are evaluated when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that that same “pre-computed” value is used for each call. This is especially important to understand when a default parameter is a mutable object, such as a list or a dictionary: if the function modifies the object (e.g. by appending an item to a list), the default value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function […]

Ok, pelo menos está mapeado. Mas a frase texto acima merece “This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function” merece destaque. Afinal, logo após a documentação descrever a funcionalidade, ela reconhece que o efeito é indesejado (afinal, parâmetro de função tem escopo local), e ainda por cima descreve a Solução Rápida de Eficiência Duvidosa, a famosa Gambiarra (ou workaround, no jargão técnico). Ou seja, o problema poderia ser resolvido na fonte e as funções de Python poderiam ter o mesmo comportamento das funções de outras linguagens.

Infelizmente, acabei de dar uma olhada na doc do futuro Python 3.0, e a feature permanece……

Variações sobre o tema Twitter

Hoje de manhã li uma notícia sobre o Twitter que gerou várias discussões interessantes com várias pessoas sobre vários temas que considero relevantes. Achei que seria interessante repassar os pontos discutidos aqui.

Tudo começou com uma discussão sobre as causas da instabilidade que vem assombrando o Twitter nos últimos dias. Fenômeno da internet, o microblog parece estar sofrendo dos altos índices de popularidade e tem passado mais tempo fora do ar do que dentro dele (aliás, neste momento em que vos falo, o dito cujo nem responde com a tradicional tela de erro).

Quem acabou levando a culpa foi o também famoso Ruby On Rails, que serve de base para o funcionamento do site. Foi noticiado que eles estariam com problemas de escalabilidade (ah… a escalabilidade… suspiros), e que a equipe de desenvolvimento estaria considerando migrar para PHP ou Java. Obviamente os advogados de Java de plantão se uniram e começaram a malhar o Rails. Afinal, como todos sabemos, “só Jesus salva, e só Java escala.”

Neste momento, cabe um comentário: programo em Java desde 96, adoro a linguagem e inclusive ganho o meu pão de cada dia com ela. Mas esse mito da escalabilidade única de Java me dá calafrios…

E ainda cabe outro comentário: a dúvida entre PHP e Java é pouco lisonjeadora pra esta última….

Voltando: eu não conheço RoR, não conheço Ruby, não sou fã da sintaxe da linguagem, e sinceramente não tenho motivos pra defender ou atacar nenhum dos dois. Mas, como diria meu colega Raphael: linguagem não escala, quem escala é a arquitetura do sistema, e parecia óbvio que havia um problema de arquitetura e infraestrutura por trás.

E não deu outra. No final de maio, o próprio blog do Twitter publicou uma entrevista comentando os problemas que eles vem enfrentando e ficou claro que no caso deles, o buraco é bem mais embaixo:

Q: Is it true that you only have a single master MySQL server running replication to two slaves, and the architecture doesn’t auto-switch to a hot backup when the master goes down?
A: We currently use one database for writes with multiple slaves for read queries. As many know, replication of MySQL is no easy task, so we’ve brought in MySQL experts to help us with that immediately. We’ve also ordered new machines and failover infrastructure to handle emergencies.

Resumindo: o Twitter tem como base 3 computadores!!! Provavelmente eles são adeptos da famosa frase do Knuth que diz que otimização prematura é fonte de problemas. Eu também concordo com isso, mas com ressalvas. Acho que existem otimizações maduras que são necessárias e sempre bem vindas.

Mais pra frente, existe um outro ponto que achei interessante: Segundo o artigo, eles contrataram alguns ex-engenheiros do Google, que irão trabalhar na escalabilidade do sistema, e em particular, irão migrar para um sistema de armazenamento de dados baseado em sistema de arquivo, substituindo o sistema de base de dados.

Você sabia que o Google não usa base de dados para o seu incrível sistema de buscas? Pois é, tudo se baseia no BigTable, uma espécie de mega-arquivo distribuído que entre outras coisas não permite operações de joins, por questões de escalabilidade e velocidade.

E isso suscitou uma outra discussão com um colega de trabalho: hoje em dia, pensou em persistência, pensou em sistema de base de dados. Mesmo que isso não seja realmente necessário.

Sistemas de base de dados são ferramentas extremamente úteis, eficientes e importantes no dia a dia da computação, sobretudo quando falamos de sistemas web. Mas é sempre bom lembrar que as vezes um simples arquivo em disco resolve muito bem. Quando? Quando não precisamos de operações como buscas, joins, filtros, histórico….

Exemplo: no job4dev, o sistema de envios de mensagens para o Twitter é assíncrono. Ou seja, a vaga é adicionada no sistema, e de tempos em tempos uma tarefa cron (sim, o bom e velho crontab do linux que funciona perfeitamente) pega o título e url das vagas e envia para o microblog. Para simplificar minha vida, eu salvo as informações que eu quero enviar em um arquivo (cada vaga é gravada em um arquivo diferente) em um diretório pré-determinado, que a tarefa cron lê. Simples, eficiente. E aposto que 95% das pessoas que fizessem isso pensariam de cara em salvar num BD, só para ter que fazer um select depois.

Outro exemplo de uso de BD que eu achava inútil: durante muito tempo eu trabalhei em uma empresa que desenvolve soluções para análise de dados biológicos. Um dos nossos sistemas era um visualizador de lotes de seqüências genéticas. Obviamente, todos estes dados estavam armazenados em uma linda base de dados normalizada. Para carregar um lote, era necessário sempre fazer uma enorme quantidade de joins e selects. O detalhe é que NUNCA o sistema fazia buscas nestas sequências, NUNCA o sistema filtrava apenas alguns dados do lotes, e SEMPRE o lote era carregado de uma vez e salvo de uma vez, em um sistema Desktop onde o gerenciamento de dados em memória é bem simples. E ainda assim, os meus apelos para usarmos arquivos em disco foram inúteis.

O que eu gostaria de deixar como ponto importante deste post que as vezes é bom pensar fora da caixa, procurar novas abordagens para resolução de problemas, sair um pouco da rede de proteção que algumas ferramentas já bem estabelecidas supostamente oferecem. Estas ferramentas são boas, não tem como negar isso e dizer que elas não servem pra nada. Elas servem, e muito.

Mas o status quo não leva a evoluções. Se você dissesse hoje que iria fazer um sistema de buscas de páginas sem usar BD, provavelmente muitos diriam que você é louco.

Conversando com o Twitter

Eu já divulguei aqui que o Job4Dev tem integração com o sistema de microblog Twitter, através do usuário job4dev, onde são enviadas as ofertas de emprego.

O que eu não divulguei ainda é que desde terça está no ar mais um projeto que contou com a minha participação: sigaseutime.com.br. A idéia é bem simples: utilizar o Twitter para agregar notícias e informações de jogos ao vivo do seu time de futebol favorito. Criamos contas para diversos times do Brasil e do mundo (sigaCorinthians, sigaPalmeiras, liveBarcelona, e por aí vai. Acesse o site para o obter a lista completa).

O que eu não contei também é que fazer integração via software com o Twitter é extremamente simples, graças à API REST de acesso. REST? REpresentational State Transfer, termo criado por Roy Fielding, computeiro americano e um dos principais autores da especificação do protocolo HTTP, em sua tese de PhD.

O próprio REST tem como pilar o protocolo HTTP. A idéia básica é definir que existem recursos (fontes de informação) que podem ser acessados através de um identificador global (que no caso do HTTP, é conhecido como URL), e que retornam uma representação da informação (XML, JSON, etc). Os recursos são considerados objetos, e o REST utiliza as ações do protocolo HTTP para agir sobre estes objetos: GET, POST (as mais famosas, para quem trabalha com sistemas web e com ajax), PUT e DELETE.

  • GET URI corresponde à operação de leitura de um objeto
  • POST URI correponde à operação de criação, atualização ou remoção de um objeto, utilizando dados enviados pelo comando.
  • DELETE URI corresponde à operação de remoção de um objeto.
  • PUT URI corresponde, assim como POST, à operação de atualização ou criação de um objeto.

Assim como no HTTP, o REST não armazena sessão: o acesso tem que ser feito sem que o recurso precise ter conhecimento de requisições passadas, proxys, caches e outros recursos utilizados em sistemas web. Ou seja: qualquer serviço REST pode ser acessado apenas com a URI apropriada e a ação desejada.

A API REST do Twitter permite acesso a todas as funcionalidades do sistema, de forma extremamente simples. Por exemplo: caso eu queira listar as mensagens recebidas pelo usuário job4dev em formato xml, basta acessar a URL http://twitter.com/statuses/friends_timeline/job4dev.xml. O sistema retorna informações também em JSON (neste caso, bastaria mudar o xml do final por json).

A maioria dos comandos requer autenticação, que no caso do Twitter é feita usando o Basic Auth: a informação de usuário e senha é enviada em um header chamado Authorization, cujo conteúdo é uma string na forma Basic <dados em base 64>, onde <dados em base 64> é usuario:senha codificado em base64 (formato muito utilizado para transmissão de dados na web).

Vou colocar aqui um exemplo em Python para enviar uma mensagem nova para uma conta no Twitter. A função encodestring é responsável por converter para base64, e a função urlencode gera uma string no formato correto para colocar na requisição HTTP:

auth = encodestring('%s:%s' % (user, password))[:-1]
header["Authorization"] = 'Basic %s' % self.auth
encoded_post_data = urlencode({"status":status})
req = Request(TwitterSender.url, encoded_post_data, header)
url_data = urlopen(req)

O mais interessante desta API é que além de ser simples, ela é muito bem documentada: http://groups.google.com/group/twitter-development-talk/web/api-documentation

Frase do Dia

“Customers don’t know what they want, much less what they need, until they see it.”

Infogami

Essa vida de blogueiro “4Dev” é dura. Os planos de dominação mundial do editor-chefe dão dor de cabeça para os tais “colaboradores”. Ter que trabalhar de ghost-writer é pouco. Agora a gente tem que bancar o ghost-developer. Já não basta ter a cara de pau de pedir pra gente escrever 32 artigos por semana, ele ainda me pediu na semana passada para que eu fizesse um site que fosse um banco de banco de dados de várias empresas de tecnologia. Parece que ele está querendo expandir os recursos e funcionalidades de certo site de empregos, e um recurso que pode ser interessante para o usuário é ter um link que contenha informações relevantes a respeito da empresa.

Enquanto eu limpava as feridas causadas pelas chicotadas recebidas durante tal singelo pedido, perguntei de onde viriam os dados sobre as empresas. A resposta não poderia ter sido mais brilhante:

Seu imbecil! Todo mundo sabe que a onda do web2.0 é o crowdsourcing. Deixe que os próprios usuários do sistema coloquem os dados sobre as empresas que já conhecem. Pra que pagar por algo quando alguém pode fazer por você, de graça?

Foi aí que eu percebi que o editor-chefe não queria um banco de dados, mas ele queria um sistema parecido com um wiki, onde os usuários pudessem criar novas entradas e editá-las posteriormente. Deu certo com a Wikipedia, pode dar certo para uma compilação de informações sobre empresas, não é mesmo?

Sistemas “wiki” existem aos milhares. Permitem que você crie novos documentos livremente, que edite e mantenha um histórico de alterações, protegem contra usuários maliciosos que vandalizam o site, etc, etc, etc. É uma proposta simples e eficiente para facilitar a captação de dados vindos de vários usuários.

Por outro lado, um simples wiki serve apenas como repositório de documentos. Arquivos simples de texto. Não há forma de estruturar a informação de maneira consistente, a não ser que você obrigue cada usuário a seguir um padrão de documento que possa ser processado por um programa.

A alternativa seria um sistema que usasse os princípios do wiki, mas que pudesse ser aplicado a outros “tipos” de dados além de blocos de texto. Para alegria do editor-chefe e salvação de meu lombo, tal alternativa existe: Infogami.

O Infogami permite que você defina tipos estruturados. Depois, pode-se indicar qual é o tipo de cada recurso (uma URL pode ser a representação de um tipo “documento”, outro pode ser do tipo “usuário”, outra pode ser do tipo “Foto com legenda”, etc), e esses recursos podem ser adicionados, editados e removidos livremente, como num wiki.

Existe também a possibilidade de alterar a representação visual para cada tipo: a página que mostra um documento pode ser totalmente diferente da página que mostra os dados de um usuário, por exemplo. E também podemos estender o sistema com plugins e macros para automatizar ações e operadores que atuam nos tais tipos.

Se você quiser ter uma idéia do potencial desse sistema dê uma olhada na OpenLibrary, que é um site que é “Powered By Infogami”, além do Jottit, que é o sistema mais simples que eu conheço para colocar uma página online.

É praticamente certo que eu vá usar o Infogami como plataforma para o desenvolvimento dessa base de dados de empresas. Só não está pronto pelo fato do editor-chefe, em Sua Infinita Benevolência, ter permitido que eu me dedique a outro trabalho nos últimos dias… tipo assim, um que seja capaz de pagar as minhas contas…

Next Page »