segunda-feira, junho 27, 2005

Seja compativel

Uma das características mais marcantes de produtos de TI bem sucedidos, como em muitas areas e diferente de tantas outras, é que eles trazem um grande diferencial de qualidade em relação aos concorrentes. Claro, depois de certo tempo a tendência é que a concorrência aperte o cerco. Mas, por algum período, cada vez menor, diga-se de passagem, o produto inovador tende a dominar sozinho o mercado. Outro fator bastante importante para o sucesso de um produto de tecnologia da informação é a compatibilidade com produtos de terceiros, geralmente tão competitivos quanto. Alias, compatibilidade é algo que movimenta qualquer setor. Carros agora não são compatíveis apenas com a gasolina, o que aquece o mercado de Álcool e, se eu parar para pensar melhor, creio que chegarei à conclusão de que convergência digital tem futuro justamente por criar compatibilidades: entre a TV e o computador, entre o telefone e o computador, entre o telefone e o cartão de crédito, entre o controle remoto e o abridor de latas, entre a bohemia e corações de galinha bem temperados.

Mas antes que eu elocumbre demais em torno de convergência, deixe-me voltar para o propósito real desse post: a compatibilidade com terceiros garante maior competitividade. Apesar de não ser um conceito novo, parece-me que apenas agora a comunidade open source se deu conta realmente disso e a ideia tem se tornado algo bem mais comum. Assim que cada vez mais frameworks de A trabalham fácil com os de B, C, D e E. E não é apenas uma característica de reuso, é algo além disso. É a compatibilidade pensada antes para aproveitar a popularidade alheia a fim de ganhar competitividade. No mundo Java, não posso deixar de citar o Spring, que surgiu com suporte ao Hibernate, iBatis, Velocity, e por aí vai. Não dá para deixar de citar também o WebWork, prontinho para se aconchegar ao Hibernate, também, Jasper, FreeMarker, Pico e, ora vejam só, Spring. Cada vez mais frameworks seguem esse caminho. Até Java por si só nasceu assim: run anywhere. Quer compatibilidade maior?

Fora isso, vale citar a movimentação do mercado em torno de se permitir funcionar no Linux, MacOS, FreeBSD e não apenas no Windows. Só para lembrar, compatibilidade foi o que fez o Windows ir além dos Mac OS da vida, ao menos quando o assunto é mercado. Abrir as APIs e deixar que os fabricantes de softwares fizessem, mais fácil e rapidamente, programas Windows, foi crucial. Agora, um pouco do esforço, proporcional a sua popularidade, se volta para o Linux. O problema é que, aparentemente, a historia do Linux caminha no sentido oposto, o de criar tantas distribuições a ponto de criar incompatibilidades. Tomara que o "pai" do software livre aprenda algo que "filhos" que estão nascendo sabem de cor. E agora o outro lado da moeda. Criar incompatibilidades para promover inovação. Não exatamente isso, mas, pense bem, para que diacho adotar um padrão se ele simplesmente não é bom? Não faz sentido e as grandes empresas perceberam isso e tomaram atitudes de criar comitês, laboratórios ou projetos comuns para desenvolver padrões de acordo com o mercado que nem sempre segue as regras definidas pelo W3C, só para exemplificar.

Um fato talvez não tão claro a primeira vista é que, geralmente, vc precisa ser uma empresa com uma boa, mas muito boa mesmo, fatia do mercado para criar uma incompatibilidade. Por exemplo, a Microsoft lançou o Windows XP e ele simplesmente era incompativel com muitos produtos, em especial hardware ainda presentes no mercado. Muitos usuarios reclamaram, entretanto, ninguem duvida que o Windows XP é hoje o sistema operacional desktop mais usado e se duvida, deve ser por picuinha. Fora a movimentação de mercado dos fabricantes de hardware e software para se adequarem ao novo modelo e se manterem compativeis com um produto de sucesso. Enfim, a Microsoft podia se dar ao luxo de fazer algo assim porque está acima no mercado, pode ditar regras, mesmo que pareçam arbitrarias. Agora, tome o caso da AMD, porque o chip de 64bits não emplacou tão rapidamente quanto o XP? Se eu desconsiderar as diferenças entre um processador e um sistema operacional, simples, a lider de mercado, com boa folga, é outra empresa, a Intel e AMD se vê em uma curva S de adoção que sequer pode chegar ao fim. A Intel como boa conhecedora já aposta em outros aspectos ligados a convergencia digital e compatibilidade com terceiros, vide novo acordo com a Apple.

valeuz...

quarta-feira, junho 22, 2005

Irretocável

Ouça!

valeuz...

sexta-feira, junho 17, 2005

Ruby on Rails: embasbacante!

Se vc trabalha com desenvolvimento web, e mesmo depois de ver esse video sobre o Ruby on Rails não quiser experimentar um pouco do treco é bom procurar algum tratamento psiquiatrico! O video tem duração de ~10 minutos e vale cada um deles pela praticidade demonstrada. O cara cria um mini weblog em duas(!) classes e um pequeno HTML. Muito legal mesmo.

valeuz...

terça-feira, junho 14, 2005

Suggest com o Lucene II

Como vcs viram em um post passado, desenvolvi um pequeno esquema para realizar suggests com o Lucene. Agora eu finalmente terminei e coloquei para funcionar em um dos sistemas em que trabalhei recentemente. É incrivel como o Lucene permite desenvolver coisas interessantes sem muita complexidade, realmente simplicidade é uma vantagem forte dessa api. Vamos lá, primeiro eu refatorei a classe Suggest para guardar tanto a palavra original quanto a sugestão e o numero de ocorrências da tal sugestão. O resultado final é:

public class Suggest implements Comparable<Suggest> {

private String suggest;
private String original;
private int occurrences;

/**
* @param suggest
* @param original
* @param occurrences
*/
public Suggest ( String suggest, String original, int occurrences ) {

this.suggest = suggest;
this.original = original;
this.occurrences = occurrences;
}

/**
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals( Object obj ) {

boolean result = false;

if (obj instanceof Suggest) {

Suggest s = (Suggest) obj;

String sWord = s.suggest.toLowerCase();
String thisWord = suggest.toLowerCase();

result = sWord.equals(thisWord) && occurrences == s.occurrences;

}

return result;
}

/**
* @see java.lang.Object#hashCode()
*/
public int hashCode() {

return suggest.toLowerCase().hashCode() * occurrences;

}

/**
* @see java.lang.Object#toString()
*/
public String toString() {

return suggest + " - " + occurrences;

}

/**
* @param other
* @return
* @see java.lang.Comparable#compareTo(T)
*/
public int compareTo( Suggest other ) {

return other.occurrences - occurrences;

}

// somente gets, o objeto é immutable

}

Ter a palavra original é interessante porque a partir dela posso saber se houve mesmo uma sugestão ou não. Quando trato de uma palavra simples isso não chega a ser necessário, mas no caso de frases, é importante para que eu destaque apenas as sugestões e não todas as palavras presentes em uma consulta. Tambem refatorei algumas partes da classe Suggestor para evitar que ele sugerisse a propria palavra, fiz algumas pequenas melhorias na performance, sugestões para frases e as adaptações simples decorrentes do refactoring de Suggest. O bom da historia foi ver as classes clientes de Suggestor funcionarem perfeitamente depois disso tudo. Enfim, o novo codigo de Suggestor é:

public class Suggestor {

/**
*
*/
public static final String PUNCTUATION_SPLITER = "[\\p{Punct}\\s]+";

private static final Log logger = LogFactory.getLog(Suggestor.class);

private Directory lucenePath;
private Analyzer analyzer;
private float minSimilarity = FuzzyQuery.defaultMinSimilarity;

/**
* @param path
* @param analyzer
* @param similarity
*/
public Suggestor ( Directory path, Analyzer analyzer, float similarity ) {

lucenePath = path;
minSimilarity = similarity;
this.analyzer = analyzer;
}

/**
* @param path
* @param analyzer
* @param similarity
* @throws IOException
*/
public Suggestor ( Resource path, Analyzer analyzer, float similarity )
throws IOException {

this(FSDirectory.getDirectory(path.getFile(), false), analyzer, similarity);

}

/**
* @param word
* @param field
* @return
*/
public List<Suggest> suggestsByFrequency( String word, String field ) {

List<Suggest> suggests = suggestsBySimilarity(word, field);

Collections.sort(suggests);

return suggests;

}

/**
* @param word
* @param field
* @param maxSuggestions
* @return
*/
public List<Suggest> suggestsByFrequency( String word, String field,
int maxSuggestions ) {

List<Suggest> suggests = suggestsByFrequency(word, field);

suggests = chopList(maxSuggestions, suggests);

return suggests;

}

/**
* @param word
* @param field
* @param maxSuggestions
* @return
*/
public List<Suggest> suggestsBySimilarity( String word, String field,
int maxSuggestions ) {

List<Suggest> suggests = suggestsBySimilarity(word, field);

suggests = chopList(maxSuggestions, suggests);

return suggests;

}

/**
* @param word
* @param field
* @return
* @throws IOException
*/
public List<Suggest> suggestsBySimilarity( String word, String field ) {

IndexReader reader = null;
List<Suggest> suggests = new ArrayList<Suggest>();

try {

synchronized (LuceneMonitor.LUCENE_MONITOR) {

reader = IndexReader.open(lucenePath);
Term term = new Term(field, word);

FuzzyTermEnum termEnum;
termEnum = new FuzzyTermEnum(reader, term, minSimilarity);

suggests = termEnumToList(word, termEnum);

}

} catch (IOException ex) {

throw new LuceneIndexException(ex);

} finally {

try {

if (reader != null) reader.close();

} catch (Exception ex) {

if (logger.isDebugEnabled()) {

logger.debug("Could not close reader", ex);

}
}

}

filterSuggests(suggests, word);

return suggests;

}

private void filterSuggests( List<Suggest> suggests, String word ) {

Predicate predicate = new AvoidWordItseltPredicate(word);
CollectionUtils.filter(suggests, predicate);

}

/**
* @param phrase
* @param field
* @return
*/
public List<Suggest> phrasalSuggest( String phrase, String field ) {

List<Suggest> suggests = new ArrayList<Suggest>();

IndexReader indexReader = null;
TokenStream tokenStream = null;

try {

Reader reader = new StringReader(phrase);
tokenStream = analyzer.tokenStream(field, reader);

synchronized (LuceneMonitor.LUCENE_MONITOR) {

indexReader = IndexReader.open(lucenePath);

Token token;
while ((token = tokenStream.next()) != null) {

Term term = new Term(field, token.termText());

FuzzyTermEnum termEnum;
termEnum = new FuzzyTermEnum(indexReader, term, minSimilarity);

List<Suggest> temp = termEnumToList(token.termText(), termEnum);

if (!temp.isEmpty()) {

suggests.add(Collections.min(temp));

}
}
}

} catch (IOException ex) {

throw new LuceneIndexException(ex);

} finally {

try {

if (indexReader != null) indexReader.close();

} catch (IOException ex) {

if (logger.isDebugEnabled()) {

logger.debug("Could not close reader", ex);

}

}

}

filterSuggests(suggests, phrase);

return suggests;

}

private List<Suggest> termEnumToList( String word, TermEnum termEnum )
throws IOException {

List<Suggest> suggests = new ArrayList<Suggest>();

while (termEnum.next()) {

Term term = termEnum.term();

String termValue = term.text();
int frequency = termEnum.docFreq();

suggests.add(new Suggest(termValue, word, frequency));

}

return suggests;
}

private List<Suggest> chopList( int maxSuggestions, List<Suggest> suggests ) {

if (suggests.size() > maxSuggestions) {

suggests = suggests.subList(0, maxSuggestions);

}

return suggests;
}

/**
* @author Marcos Silva Pereira - marcos.pereira@vicinity.com.br
*
*
* @since 24/05/2005
* @version $Id$
*/
static class AvoidWordItseltPredicate implements Predicate {

private Set words;

/**
* @param words
*/
public AvoidWordItseltPredicate( String words ) {

this(makeSet(words));

}

/**
* @param words
*/
public AvoidWordItseltPredicate( Set words ) {

this.words = words;

}

/**
* @see org.apache.commons.collections.Predicate#evaluate(java.lang.Object)
*/
public boolean evaluate( Object obj ) {

boolean result = false;

if (obj instanceof Suggest) {

Suggest suggest = (Suggest) obj;
String word = suggest.getSuggest().toLowerCase();

result = !words.contains(word);
}

return result;

}

private static Set makeSet( String words ) {

Set<String> set = new HashSet<String>();

String[] strings = words.split(PUNCTUATION_SPLITER);

for (String string : strings) {

set.add(string.toLowerCase());

}

return set;
}

}
}

O metodo phrasalSuggests usa o Analyzer para parsear as palavras e evitar que eu tente fazer sugestões para stop words, por exemplo. AvoidWordItseltPredicate é uma implementação simples de Predicate, interface do Jakarta Commons Collections, e é ele quem filtra o conjunto de sugestões para evitar que a propria palavra seja sugerida. Alteradas essas classes, criei uma helper para gerar codigo HTML a partir de uma frase e seu conjunto de sugestões, nada demais como vc pode ver abaixo:

public class HTMLPhrasalSuggest {

/**
*
*/
private HTMLPhrasalSuggest () {

// private constructor to avoid instantiation...
}

/**
* @param phrase
* @param suggests
* @param tag
*
* @return
*/
public static String htmlPhrasalSuggest( String phrase,
List<Suggest> suggests, String tag ) {

StringBuilder result = new StringBuilder();

String[] wordsInPhrase = phrase.split(Suggestor.PUNCTUATION_SPLITER);

int i = 0;
for (String string : wordsInPhrase) {

if (hasSuggest(string, suggests)) {

result.append(openTag(tag));
result.append(suggests.get(i++).getSuggest());
result.append(closeTag(tag));

} else {

result.append(string);

}

result.append(" ");

}

return result.toString().trim();

}

/**
* @param phrase
* @param suggests
* @return
*/
public static String phrasalSuggest( String phrase, List<Suggest> suggests ) {

StringBuilder result = new StringBuilder();

String[] wordsInPhrase = phrase.split(Suggestor.PUNCTUATION_SPLITER);

int i = 0;
for (String string : wordsInPhrase) {

if (hasSuggest(string, suggests)) {

result.append(suggests.get(i++).getSuggest());

} else {

result.append(string);

}

result.append(" ");

}

return result.toString().trim();

}

private static boolean hasSuggest( String word, List<Suggest> suggests ) {

boolean result = false;

for (Suggest suggest : suggests) {

if (suggest.getOriginal().equalsIgnoreCase(word)) {

result = true;
break;

}

}

return result;

}

private static String openTag( String tag ) {

return "<" + tag + ">";

}

private static String closeTag( String tag ) {

return "</" + tag + ">";

}

}

No metodo privado hasSuggest dá para ver a utilidade de guardar a palavra original. Se ela está presente no conjunto de sugestões, é porque houve uma sugestão para ela. Por exemplo, "jakarta luceni" vai gerar um conjunto de sugestões apenas com "lucene" já que "jakarta" está grafada corretamente. E por fim, o uso dessa tralha toda é feito via uma tag que criei baseado no suporte do WebWork com OGNL e tudo mais:

public class LuceneSuggestTag extends WebWorkTagSupport {

private String query;
private String field;
private String url;
private String tag;

private Suggestor suggestor;

/**
* @see javax.servlet.jsp.tagext.BodyTagSupport#doEndTag()
*/
public int doEndTag() throws JspException {

try {

List<Suggest> suggests = suggestor.phrasalSuggest(query, field);

String htmlSuggest;
htmlSuggest = HTMLPhrasalSuggest.htmlPhrasalSuggest(query, suggests, tag);

String textSuggest;
textSuggest = HTMLPhrasalSuggest.phrasalSuggest(query, suggests);

StringBuilder toShow = new StringBuilder();
toShow.append("<a href=\"").append(url).append(textSuggest);
toShow.append("\">").append(htmlSuggest).append("</a>");

Writer writer = pageContext.getOut();
writer.write(toShow.toString());

} catch (Exception ex) {

throw new JspException(ex.getMessage(), ex);

}

return EVAL_PAGE;
}

/**
* @see javax.servlet.jsp.tagext.BodyTagSupport#doStartTag()
*/
public int doStartTag() throws JspException {

try {

ServletContext servletContext = pageContext.getServletContext();

suggestor = (Suggestor) servletContext.getAttribute("luceneSuggestor");

query = String.valueOf(getStack().findValue(query, String.class));
field = String.valueOf(getStack().findValue(field, String.class));

return SKIP_BODY;

} catch (Exception ex) {

throw new JspException(ex.getMessage(), ex);

}
}

// sets e gets para os atributos.

}

E o uso, no meu caso numa jsp:
<%@ taglib prefix="ww" uri="webwork" %>
<%@ taglib prefix="lucene" uri="lucene" %>

...

<lucene:suggest field="'PageText'" query="query" tag="em" url="Search.pc?query=">

...

Agora, coisas que preciso melhorar:
1. Evitar que a view precise indicar o campo a ser buscado para as suggests (PageText);
2. Suggests para palavras coladas (comunidadeblastemica -> comunidade blastemica);
3. Tornar a tag library compativel com o metodo POST do HTTP;
4. Escapes para HTML e evitar que algum mal intencionado envie queries como <javascript bla bla bla>

É isso, sugestões e comentarios são muito bem vindos.

valeuz...

quinta-feira, junho 09, 2005

Qualidade importa

Um argumento recorrente de quem não tem tanta preocupação com qualidade de software em geral é: "O cliente não quer saber dos padrões e métricas usados, quer que funcione e pronto". Isso é mais do que verdade, até porque, para boa parte dos clientes, não faz sentido explicar como os padrões do GoF, por exemplo, ajudam, quando usados de acordo, a gerar software de maior qualidade. Os clientes geralmente querem que os requisitos sejam cumpridos dentro de prazos e custos e não que se use o mais novo e balado framework. Mais ainda, apenas em casos que implicam manter compatibilidade, os clientes se preocupam com as tecnologias usadas no desenvolvimento.

Apesar de o argumento parecer extremamente lógico para boa parte dos casos, ele não é verdadeiro quando você olha para outro aspecto envolvido: a "empresa" que desenvolve. Em tempos de prazos cada vez mais apertados, desenvolver software sem se preocupar com seu futuro pode manter a empresa amarrada a um problema muito além do tempo necessário. É o típico caso de haver tempo para fazer duas (ou mais) vezes, mas não haver para fazer uma vez bem feita. Há, a meu ver, dois grandes problemas com software mal desenvolvido que, de uma maneira ou outra, irão criar outro conjunto de problemas. São eles:

1. Você passará muito tempo literalmente remendando erros e, provavelmente, criando outros. Isso toma parte da força de trabalho para algo que simplesmente não gera o lucro esperado se a gama de usuários for pequena. Para grandes empresas os custos para consertar o problema podem ser altos não apenas pelo conserto em si mas também para a marca, a percepção que as pessoas têm do produto ou empresa. Que o diga a Microsoft com o grande numero de service packs e com a falta de credibilidade que permeou o Windows por um bom tempo. E, pode acreditar, se você não tem qualidade ou credibilidade, alguém terá. No fim das contas, qualquer pessoa envolvida seriamente com qualidade, sabe que manutenção toma mais tempo do que desenvolvimento. Isso é fato e nos leva ao seguinte: bom desenvolvimento leva a um período de manutenções mais leves;

2. Outros projetos são afetados pela falta de qualidade do primeiro. Em especial, se algo não foi bem feito, provavelmente não será reusado, se for, o problema apenas aumenta já que temos uma propagação do erro. Se não é possível reusar, o tempo para desenvolvimento de novos projetos certamente crescerá e o retrabalho causado pela falta de reuso causará uma série de horas extras e daí para frente vc pode imaginar sozinho. Falta de reuso gera outro problema menos explicito que é o aumento na quantidade de erros e código a corrigir/refatorar;

Projetos com qualidade, por outro lado, têm, claro, uma série de aspectos positivos para a empresa e no fim das contas para os clientes. Para emparelhar com os problemas acima e ser um pouco chato, vou citar os mais óbvios e importantes:

1. Não passar meses corrigindo erros tem uma vantagem além da economia de tempo e dinheiro. Com bons produtos as chances de feedback e adesão de novos clientes crescem. Bom feedback é crucial para o amadurecimento não apenas do produto mas da empresa em si. Bom feedback lhe direciona para as necessidades mais importantes do cliente, lhe permite desenvolver funcionalidades não pensadas ou conhecidas antes e, então, estar hábil a conquistar novos clientes. Novos clientes, mais do que apenas mais grana, implicam mais popularidade, fortalecimento da marca e amadurecimento, agora especialmente da empresa que já pode pensar em lançar novos produtos com uma legenda do tipo "dos mesmos produtores de bla bla bla...";

2. Os tipos de vantagem trazidos pelo reuso são geralmente claras, imagine iniciar um projeto com 20% de código pronto, bonitinho. Mas, mais ainda, quando vc reusa código, percebe pontos nos quais ele pode melhorar, o contato mais freqüente lhe faz pensar sobre melhorias possíveis. É menos codigo para testar/corrigir/refatorar tambem. Sem reuso seu codigo/produto simplesmente não evolui e, novamente, daqui para frente vc pode imaginar sozinho.

Para finalizar, ficam algumas dicas simples:

1. Planeje! Mesmo planejamento mínimo é melhor do que nenhum e até metodologias ágeis como XP têm etapas para planejar o jogo. Planejamento geralmente passa por decidir escopo, atividades, como usar tempo e recursos disponíveis entre outras coisas, qualquer bom livro de gerenciamento de projetos explica como planejar. Depois, decidir e justificar, não se esqueça, quais tecnologias usar. Justificar lhe ajuda a fundamentar a escolha e evitar mudanças desnecessárias no decorrer do desenvolvimento;

2. Defina metas de qualidade reais baseadas nos recursos e tempo disponíveis. Metas irreais criarão atrasos e frustrações desnecessárias. Lembre-se da filosofia de não tentar "tirar leite de pedra". Tente criar metas gerais e, dentro destas, metas menores que devem ser alcançadas em etapas. Assim fica mais fácil...

3. ... controlar a qualidade, o que significa criar padrões e convenções a serem seguidas e verificar de maneira efetiva se são seguidas. Uma "convenção" interessante são testes. Obrigue-se a escrever testes. Só essa dica já daria para escrever outro post, testes praticamente gritam: "ei, isso aqui funciona".

4. Revise e tente fazer o codigo evoluir entre um projeto e outro. Só siga essa dica se vc seguir a 3 também. Procure identificar pontos fracos no código, pontos onde há tendencias de se usar um padrão e refatorar o que conseguir identificar. Uma boa maneira de se fazer isso é usar algumas ferramentas de inspeção de codigo como o PMD, Findbugs, Metrics e Checkstyle.

5. Faça todos os envolvidos pensarem em qualidades para não ocorrerem dissiparidades em partes do projeto. Mesmo que todos não estejam no mesmo nivel de conhecimento, boa comunicação entre os desenvolvedores pode garantir ou ao menos diminuir as chances de os menos experientes cometerem erros.

valeuz...