Seaside: um framework web de verdade

Provavelmente isso não é novidade para alguns de vocês, mas já faz algum tempo que eu venho me aventurando pelo incrível mundo do Smalltalk. Dando prosseguimento à esta saga, tenho o orgulho de anunciar a minha próxima vítima: o framework Seaside!

O Seaside é um impressionante framework web escrito em Smalltalk focado no desenvolvimento de sites aplicações stateful. Mesmo conhecendo muito pouco a respeito do seu funcionamento — até então, nada além de um mísero hello world — resolvi tentar criar uma aplicação que realmente fizesse algo a mais do que simplesmente escrever um texto na tela.

A experiência foi muito boa, mas poderia ter sido melhor se o projeto contasse com uma documentação decente. O código do framework propriamente dito é muito bem documentado e é possível aprender bastante, mesmo tendo apenas o Class Browser do Squeak como recurso de pesquisa. Mas convenhamos: uma documentação como esta ainda faz falta, principalmente para os aventureiros que, como eu, não fazem parte do core team do Seaside.

Entendendo o exemplo

Não mostrarei nada de muito complexo aqui, até porque eu também estou aprendendo. Mas o exemplo é interessante para conhecermos algumas das características principais do Seaside, que serão explicadas no decorrer deste post.

Diagrama de estados

Diagrama de estados

Você conhece aquele jogo de “adivinhar o número”? Pois então… criaremos aqui algo parecido. Ao lado, você pode conferir uma imagem que ilustra como a aplicação deverá funcionar.

Ao abrir a aplicação, o usuário irá ver uma mensagem, que deverá confirmar para que o jogo comece. Quando o usuário confirma, o sistema gera um número aleatório entre 1 e 10 e mostra um formulário, que o usuário deve preencher com seu palpite. Em seguida, o sistema checa se o palpite do usuário corresponde ao número gerado aleatoriamente: se sim, o sistema mostra uma mensagem parabenizando o usuário; caso contrário, o sistema re-exibe o formulário, mostrando a contagem de “chutes” até o momento, além de uma mensagem com uma dica, para ajudar o usuário a adivinhar o número.

Esse ciclo continua até o usuário acertar. Por fim, o sistema pergunta se o usuário deseja jogar novamente: em caso afirmativo, um novo número é sorteado e o formulário re-exibido; senão, a mensagem de boas vindas é mostrada.

Ainda em relação ao diagrama, perceba que há um quadrado vermelho em torno de algumas atividades. Basicamente, isso quer dizer que a aplicação não deve permitir que o usuário volte (com o botão back do browser) e re-submeta um palpite após já tê-lo acertado. Em outras palavras, eu quero que as atividades dentro do quadrado vermelho representem um workflow, ou transação.

Introdução

O Seaside trabalha com o conceito de componentes. Neste exemplo, criaremos dois componentes para implementar a aplicação proposta. Um será responsável por controlar o fluxo de navegação do usuário pela aplicação, e o outro que receberá o input do usuário através de um formulário. Mas, antes de prosseguirmos, vamos aprender para que servem duas das mais importantes classes do Seaside: WAComponent e WATask.

A classe WAComponent será sua melhor amiga em um projeto Seaside, pois é através da criação de sub-classes de WAComponent que construímos a aplicação, pedaço por pedaço. Toda sub-classe de WAComponent deve implementar o método #renderContentOn:, pois é através deste que definimos como o componente deve ser renderizado no browser do usuário.

A classe WATask, por sua vez, é parecida com a classe WAComponent (de fato, ela a estende), com a diferença que ela não é usada para nada além de definir uma tarefa, ou melhor, a ordem com que outras tarefas e componentes devem ser chamados de modo a cumprir um determinado objetivo. Em poucas palavras: estenda WATask para definir workflows!

Entendendo a interação entre os componentes

Como pôde ser visto no diagrama de atividades mostrado anteriormente, o fluxo começa quando o usuário confirma que deseja jogar, passando o controle para o fomulário. O formulário é então renderizado no browser do usuário para que ele possa enviar um palpite. Em seguida, quando o usuário submete o formulário, o componente chamador (que é o próprio fluxo) recebe o palpite do usuário e o compara com o número gerado no início. Dependendo do resultado dessa comparação, o formulário é exibido novamente ou a mensagem de sucesso é mostrada.

Certo… talvez tenha ficado confusa a parte em que um componente “passa o controle” para outro. Esse é um conceito muito importante em um framework como o Seaside. Para entender essa coisa do “passa o controle”, imagine uma chamada de método comum:

void main(char** args) {
    foo(); // chama o método ("passa o controle")
}
 
void foo() {
    // faz algo aqui
}

Neste exemplo de código C, quando chamamos um método, o fluxo de execução é modificado, fazendo com que o método chamado assuma o controle. Então, quando o método termina, o fluxo retorna ao método chamador.

Simples não? Agora imagine isso funcionando na web. Imagine que o método main do exemplo fosse um componente da nossa aplicação, e que esse componente pudesse passar o controle para outros componentes, assim como a chamada de método no exemplo, só que com a possibilidade de manter essa pilha de execução entre as requisições HTTP! Isso resolveria um monte de problemas, não é?

Felizmente, é assim que o Seaside trabalha. O conceito por trás disso é chamado de Continuation. A imagem a seguir tenta ilustrar o funcionamento disso no contexto do nosso exemplo:

Sequência de requisições

Sequência de requisições

Se desconsiderarmos por um momento o modelo (limitado) de request/response no qual o protocolo HTTP se baseia, a sequência de interações do usuário para com a aplicação se parece com o especificado no diagrama acima. No diagrama, é como se tivéssemos uma pilha de chamadas que não se perde entre as requisições!

O foco deste post não é explicar a teoria por trás desses conceitos todos, portanto, se você tem interesse em saber mais sobre o assunto, use o Google! :)

Definindo o fluxo da aplicação

Vamos criar o componente responsável por controlar o fluxo de navegação da aplicação. O componente a seguir estende WATask e possui as variávies de instância number e form que servem, respectivamente, para armazenar o número gerado e o formulário usado para receber o input do usuário:

WATask subclass: #GuessingAppComponent
    instanceVariableNames: 'number form'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'GuessingApp'
 
GuessingAppComponent class>>canBeRoot
    ^true
 
"Accessors for instance variables"
 
GuessingAppComponent>>children
    ^ Array with: self form
 
GuessingAppComponent>>initialize
    "Configure initial values"
 
    self number: 10 atRandom;
        form: (GuessNumberForm new)
 
GuessingAppComponent>>go
    "Start application workflow"
 
    self inform: 'Start the game pushing the button below!!'.
    [self initialize;
        [self call: self form.
            self form number < self number
                ifTrue: [self form message: 'Try a higher value']
                ifFalse: [self form message: 'Try a lower value']]
                doWhileFalse: [self form number = self number]]
        doWhileTrue: [self confirm: 'You won the game using ' , self form guesses asString , ' guess(es)!! Play again?']

Todo o código é bem simples de se entender, com exceção do método #go. Veja que o corpo do método não passa de uma transcrição dos diagramas mostrados anteriormente. Atenção especial para a chamada self call:, que é o equivalente ao exemplo de chamada de método, com a diferença que estamos transferindo o controle para que outro componente possa executar no lugar do componente corrente. Perceba também que este método ainda não está pronto, pois, do jeito que está, a aplicação não está preparada para caso o usuário queira re-submeter um palpite após já tê-lo acertado… faremos isso depois!

Recebendo o input do usuário

Abaixo segue o código do componente usado para receber o palpite do usuário. O componente estende WAComonent e tem as variáveis de instância number, message e guesses. Essas variáveis servem, respectivamente, para: guardar o palpite do usuário quando o formulário é submetido; guardar a dica a ser exibida para o usuário (algo do tipo “chute um valor mais alto”, ou “mais baixo”); e guardar o número de vezes que o formulário foi submetido:

WAComponent subclass: #GuessNumberForm
    instanceVariableNames: 'message number guesses'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'GuessingApp'
 
"Accessors for instance variables"
 
GuessNumberForm>>increment
    "Increments the number of guesses"
 
    self guesses: self guesses + 1
 
GuessNumberForm>>initialize
    "Configure initial values"
 
    self number: 1;
        message: 'Try a number between 1 and 10';
        guesses: 0.
 
GuessNumberForm>>renderContentOn: html
    "Render component"
 
    html heading: self message level: 4.
    html heading: 'You did ' , self guesses asString , ' guess(es) until now' level: 5.
    html form
        with: [html textInput value: self number;
                callback: [:value | self increment; number: value asInteger; answer].
            html submitButton value: 'check my guess!!']

Novamente, a simplicidade impera. ;) A única coisa deste código que vale explicar é o método #renderContentOn:, que serve para que possamos definir o que deve ser retornado ao usuário quando o componente precisar ser renderizado. O método recebe um objeto WAHtmlCanvas por parâmetro, responsável por gerar código XHTML automaticamente. Para quem tem conhecimentos em HTML, o código é bem trivial, com exceção do método #callback, onde colocamos algum código para ser executado quando o formulário for submetido. Perceba a chamada a self answer no final da linha, que é responsável por devolver o controle ao componente chamador (o que executou self call: form).

Demarcando uma transação

Se lembra do quadrado vermelho no diagrama de atividades? Então… precisamos dar um jeito de impedir que o usuário volte a dar palpites após ter descoberto o número. Ou seja: precisamos definir uma transação.

Isso é feito utilizando o método #isolate:, conforme pode ser visto abaixo:

GuessingAppComponent>>go
    "Start application workflow"
 
    self inform: 'Start the game pushing the button below!!'.
    [self initialize;
        isolate: [[self call: self form.
            self form number < self number
                ifTrue: [self form message: 'Try a higher value']
                ifFalse: [self form message: 'Try a lower value']]
                doWhileFalse: [self form number = self number]]]
        doWhileTrue: [self confirm: 'You won the game using ' , self form guesses asString , ' guess(es)!! Play again?']

Não tem mais código, não! É só envolver o trecho desejado em um bloco e passá-lo ao método #isolate:! :D

O resultado

Confira abaixo algumas screenshots da aplicação em funcionamento:

Tela inicial

Tela inicial

Lendo o palpite do usuário

Lendo o palpite do usuário

Tela de acerto

Tela de acerto

Back button não permitido após término do jogo

Back button não permitido após término do jogo

O que há de errado com a última screenshot? Nada… é isso que acontece quando o usuário, após já ter adivinhado o número secreto, volta algumas telas com o botão back do browser e tenta re-submeter um formulário. Não cheguei a pesquisar, mas acredito que esta página seja customizável.

Desafio

Mesmo em uma aplicação simples como a que foi descrita aqui, fica evidente o quanto o Seaside é produtivo e simples (embora eu ainda não saiba usá-lo direito). :P

Portanto, proponho aos leitores deste blog que pensem no que deve ser feito para se criar uma aplicação igual (com as mesmas funcionalidades) a mostrada aqui (com a sua linguagem e framework preferidos). Tirando um ou outro, imagino que dê para contar nos dedos de uma só mão os frameworks capazes de fazer tanto com tão pouco. Enfim, sintam-se a vontade para expressar suas opiniões a respeito disso.

Conclusão

  • Não precisei apelar para frameworks externos só para definir o workflow da aplicação;
  • Não precisei fazer gambiarras para controlar o estado da aplicação (lembrando que a aplicação suporta diversas instâncias do browser acessando a mesma aplicação concorrentemente);
  • Não precisei fazer gambiarras para tratar o botão back do browser;
  • Não precisei fazer gambiarras para que o código XHTML gerado fosse válido;
  • Não precisei ficar reiniciando o servidor web ou fazendo re-deploy o tempo todo;
  • Não precisei pagar para conseguir um bom ambiente de desenvolvimento, pois o Squeak sozinho faz muito bem o serviço;

É… acho que esses motivos já são o suficiente para que você dê uma olhada no Seaside. Se não for, santo Deus, o que mais você quer?! :)

Veja também:

Tags: , , , , , , , , , ,

Deixe um comentário