O Estado da arte da DSL em Ruby

Estive na apresentação de Glenn Vanderburg, no QCon London 2009, que falou sobre o Estado da Arte da DSL em Ruby. A apresentação e os slides você pode conferir aqui no site da infoq.

A idéia de fazer uso de uma DSL interna originou-se aparentemente no Lisp. Em Lisp você não escreve seu programa apenas direcionado na linguagem, você também constrói a linguagem em cima do seu programa:


(task “warn if websites is not alive ”
  every 3 seconds
  starting now
  when ( not (website-alive? “http://example.org”))
  then (notify “admin@example.org”, “server down!”))
)

DSL interna também foi um objetivo de design do Haskell


keepleft (p :>: ps)
  | keepleft p = case partitionFL keepleft ps of
    a :> b -> p:>: a :>: b
  | otherwise = case commuteWhatWeCanFL (p :> ps) of
    a :> p’ :> b -> case partitionFL keepleft a of
      a’ :> b’ -> a’ :> b’ +>+ p’ :>: b

Agora falando em Ruby, uma das principais características da linguagem é a expressividade. O japonês Yukihiro Matsumoto (Matz), criador da linguagem, sempre teve como objetivo fazer o ruby extremamente legível. Para atingir esse objetivo a linguagem tem o recurso conhecido como “Sintax Sugar”. A performance da linguagem não foi o objetivo inicial, e sim a clareza. Por isso o ruby é uma linguagem mais lenta. Entretanto agora com a adoção maior nos projetos de mercado direcionados a web, a preocupação com performance cresceu e o resultado disso foi o recente release da versão 1.9.1 do ruby, com resultados impressionantes de performance.

Deixando a performance de lado e voltando à DSL, em um projeto Rails você pode fazer as seguintes associações em uma classe model:

has_many :favorites, :conditions => {:state => ‘public’}

has_many :roles, :through => :projects, :uniq => true

validates_length_of :login, :within => 3..40, on => :create

validates_presence_of :authority, :if => :in_leadership_role, :message => “must be authorized for leadership”

Fica claro e limpo que o modelo tem uma associação a duas coleções (favorites e roles), e validações ficam explícitas no próprio model. Outro exemplo usado na apresentação citada acima:

#Um intervalo de tempo:

3.years + 13.days + 2.hours

# Quatro meses de agora, na segunda_feira

4.months.from_now.next_week.monday

Muita coisa é possível fazer no ruby por causa do “method_missing” que existe nos objetos. Por exemplo, você pode sobrescrever esse método e incluir um código como esse:


def element(element_name, opts={})
  write “<#{element_name}#{encode_opts(opts)}”
  if block_given?
    puts ”>#{yield}”

  else
    puts “/>”
  end
end

E o resultado disso pode ser fazer uma manipulação simples de html:

xml.html {
  xml.head{
    xml.title(“History”)
  }
  xml.body{
    xml.h1(“Header”)
    xml.p(“paragraph”)
  }
}
Outro exemplo que eu adoro é o uso de Active Record  do Rails. Graças ao “method_missing” podemos fazer uso de métodos que não existem, como por exemplo:

Funcionario.find_by_nome_and_cargo( "Fulano", "Gerente")

E ainda fazer uso de Named Scopes:
Funcionario.gerente.pelo_nome

Na linha acima o Active Record vai trazer todos gerentes ordenados pelo nome.

Ruby é uma linguagem muito boa para escrever DSL internas por ser uma linguagem não-obstrusiva e permite que muitas pontuações sejam opcionais. Porém, DSL não faz o seu software magicamente melhor e devem ser usados com precauções. Nem sempre o código fica mais limpo com esse uso.
Glenn fala sobre a complexidade do software e a indicação do famoso livro de DDD do Eric Evans, e do livro The Mythical Man-Month.

Um bom design de software:
  • Elimine tudo que possível da complexidade acidental
  • Separe o resto

Outras frases muito interessantes dessa apresentação:

  • Linguagens são para pessoas entenderem o domínio
  • Coisas que são implícitas na verdade são complexidades acidentais
  • Aprender a linguagem ajuda a entender o domínio

Um bom design de API:

  • Criar uma DSL para determinada construção é poderoso
  • Você pode refatorar ela quando achar duplicação, complexidade, etc
  • DSL interna é apenas uma parte do bom design da API no Ruby



Publicado em linguagens, QCon, rails. Tags: , . Leave a Comment »

Escalabilidade != Performance

A Escalabilidade é um requisito não funcional de arquitetura que deve ser considerado em todos os projetos porém, dependendo das necessidades do cliente (requisito funcional) e da quantidade de usuários, a escalabilidade pode um peso maior ou menor dentro do projeto.

Muita gente confunde escalabilidade com desempenho (performance) mas são duas coisas muito diferentes, apesar se um influenciar o outro. Performance é a capacidade de atender requisições dos usuários em tempo de processamento e consumo de memória aceitáveis conforme as necessidades do cliente. Já a escalabilidade é a capacidade de manter a disponibilidade e o desempenho a medida que a carga transacional aumenta. Você pode ter um sistema altamente performático em uma máquina mas que é impossível de escalar em uma arquitetura de cluster, por exemplo. Pensar em escalabilidade não é somente pensar em aumentar máquina. É pensar em IO, memória, requisição, banco de dados e assim por diante. Por exemplo, uma aplicação web que guarda muitos dados na sessão pode ser bem performática, mas se essa aplicação passar a ser usada como um web site, com milhares de usuários, a quantidade de informação na sessão (memória do servidor) tornará o sistema lento! Um HttpSession é muito difícil de escalar se for fazer uma estratégia na mão. Nesses casos uma estratégia de cache distribuído, como o memcached, poderia resolver o problema.

Tive uma experiência recente com uma aplicação web que estava apresentando problema de performance, então precisei fazer uma análise para descobrir a causa. Infelizmente essa aplicação também tem problema de manutenabilidade e nessas horas os autores nunca trabalham mais no projeto e não deixaram o endereço da casa deles para quem for dar manutenção! 🙂

Analisando o código, de cara já vi que teria problemas pois o sistema não tem testes unitários. Com certeza haveria a necessidade de fazer refatorações e nem ao menos posso garantir que não vou “quebrar” o software. Os testes de unidade e/ou de integração são tão importantes quanto o projeto em si pois são eles que garantem a qualidade do software. Um programador profissional deve escrever testes, como já disse Phillip Calçado aqui! Como não há testes, a refatoração foi dividida em sprints bem curtos de no máximo uma semana. Já que o projeto está em produção, é interessante construir testes de falha e depois testes de sucesso, pelo menos para as funcionalidades mais importantes. O ideal seria que a equipe tivesse trabalhado com TDD.

Essa aplicação, escrita em Java, apresentava problemas arquiteturais de performance mesmo com uma infra poderosa com vários clusters e load balancing, e consequentemente afetou a escalabilidade pois o aumento transacional causou indisponibilidade do sistema. Portanto, com o aumento de usuários, entrada de novas funcionalidades e crescimento de registros no banco de dados, a aplicação começou a travar e apresentar muitos problema de lentidão.

Antes de começar a fazer qualquer alteração precipitada eu baixei o JProfiler para tirar uma primeira análise. A ferramenta é boa mesmo! Monitorar consumo de memória, tempo de processamento, hotspot, trabalho do garbage collection, consultas demoradas no banco de dados etc. É uma boa prática monitorar frequentemente as aplicações com um profiler para saber como está indo o desenvolvimento, evitando gargalos desnecessários. Essa prática também evitaria otimizações prematuras, como disse Joshua Bloch aqui. Por exemplo, muita gente altera concatenação de String para StringBuffer, mas nem sabe que internamente a concatenação de string seria transformada em StringBuilder pelo java.

Voltando à aplicação, de nada adiantaria sair alterando o código se o problema estiver no banco de dados. Precisei criar índices das tabelas e com a ajuda do JProfiler isso ficou bem fácil. Já na parte da aplicação, a ferramenta também me ajudou a encontrar problemas como:

  • Excesso de instanciação de objetos dentro de loops bastante grandes. Muitos desses objetos são apenas para trafegar dados dentro da aplicação. Eles não tem inteligência nenhuma, são como fantoches. Mas o que o JProfiler aponta é que o garbage collector está precisando fazer Full GC toda hora, e isso gasta tempo!
  • Muito uso de Reflection do java dentro de loops bastante grandes. Esse recurso é muito poderoso, mas deve-se tomar algumas precauções. O desempenho da aplicação é melhor quando a chamada de um método é feito da maneira convencional. Se for para dar produtividade e o overhead não for tão grande, vale a pena. Por outro lado o uso de reflection pode deixar o código difícil de entender. No caso da aplicação em análise uma solução mais Orientada a Objetos eliminaria esses impulsos por querer dificultar problemas que seriam simples. Só para uma curiosidade, o Reflection foi rescrito no java 1.4 para melhorias de performance.
  • Comparação de strings para buscar um objeto em uma lista. Essa busca de objetos pode ser otimizada pela própria linguagem utilizando, por exemplo, o método contains de um collection. Um loop utilizando Java.util.Vector pode ser transformado em um Java.util.HashSet para buscar o objeto mais rapidamente se no caso não importar a ordem dos objetos. Porém se a intenção for fazer cálculos com atributos de uma classe, como ocorre em algums pontos da aplicação, a idéia seria que a própria classe (entidade) faça o cálculo a partir de seus atributos. Isso, além de ser mais Orientado a Objetos, eliminaria a necessidade de fazer loops para buscar o objeto e setar um valor nele.
  • Loops desnecessários. Alguns loops podem ser eliminados apenas com uma solução mais orientada a objetos. Outros podem ser eliminados apenas alterando a consulta no banco de dados para já trazer um resultado calculado, por exemplo.
  • Problemas em aberturas e tempo de vida de transações de banco de dados. Esse com certeza é um dos piores mas não vale a pena eu comentar pois a solução depende de cada aplicação.
Aplicações em Ruby on Rails sofrem com a falta de thread safety, o que pode prejudicar a escalabilidade. Mais o pessoal está escalando uma grande aplicação em Rails fazendo um uso extensivo de cache de fragmentos. Que tal o GUJ rodar em Rails 7 vezes mais rápido do que roda em java? Segue o link do bate-papo.
Conclusão:

Tem muita gente dizendo que Ruby on Rails não escala por causa do case do twitter mas tá aí uma prova que mesmo uma aplicação em Java, com recursos como GC, JIT e hotspot pode não escalar, pois não é a linguagem que não escala, e sim o modelo que não foi pensado para escalar.