Modularização de aplicações web feitas com Perl Catalyst | astux
Exporemos a arquitetura de uma aplicação web feita de maneira modularizada, ou seja, de maneira a separar 1) o software que resolve um problema qualquer do 2) software que gerencia sua interface web (e outras interfaces). Explicaremos quais as vantagens de se modularizar. Exemplificaremos para melhorar o entendimento.
Esse texto reutiliza muito da idéia exposta no livro "The Definite Guide to Catalyst", por Kieren Diment e Matt S Trout, com Eden Cardim, Jay Kuri e Jess Robinson.
Conhecimento prévio e uso de palavras
Pressupomos que o leitor tem uma base intermediária em programação de computadores, em algum framework web e está usando um sistema operacional baseado em Unix, como o Debian GNU/Linux.
Usaremos a palavra aplicação para significar uma aplicação/software/programa qualquer, que resolva um problema qualquer (ex. identificar se um composto químico é orgânico). Usaremos aplicação web para significar uma aplicação qualquer e sua interface web.
Definição de modularização
Modularização aqui significa que a aplicação (aplicação que resolve um problema) deve ser feita de forma que não "saiba" da existência de qualquer interface sua. No entanto, se estamos fazendo uma aplicação que será acessada por uma interface web, por que fazer com que ela não "saiba" de sua interface? Isso é possível e faz sentindo? Essa pergunta leva a pensar na expressão "aplicação web".
Se bem observarmos, veremos que, por exemplo, os termos "aplicação web" e "aplicação educacional" não são do mesmo tipo. "Aplicação web" diz respeito a uma aplicação qualquer que tem uma interface web; o qualificador na expressão diz respeito ao modo de acesso (interface) à aplicação. No caso de "aplicação educacional", o qualificador não diz respeito ao modo de acesso, mas sim à aplicabilidade da aplicação. Isso nos mostra que uma aplicação web não é algo monolítico, mas sim uma aplicação (que resolve um problema) e uma interface web (para acessarmos a aplicação). Existem aí "dois softwares", e o que queremos é modularizá-los (separá-los), mas fazendo com que se comuniquem. Isso é possível e faz sentindo; o porquê está nas próximas seções.
Sabendo que faz sentido pensar na aplicação como algo diferente de sua interface, devemos perguntar que vantagens se tem em separar essas duas partes.
Ao separar aplicação e interface(s), escrever testes para a aplicação fica mais fácil. Os testes poderão despreocupar-se completamente com lógicas e códigos de interface(s), concentrando-se apenas na aplicação (que resolve um problema).
Testes são algo muito importante com que devemos nos familiarizar. Os minutos que usamos escrevendo alguns testes dão qualidade ao software que escrevemos e tiram nossa preocupação quando temos que modificar o software.
O tempo usado para escrever testes hoje pode amanhã implicar em uma economia de uma quantidade de tempo muito maior. Ex. um erro que teria ido para produção não fosse um teste que o indicou. O erro em produção poderia demandar muito mais tempo de correção que o tempo usado para escrever o teste.
Acesso com outras interfaces
Considerando que a interface web de uma aplicação é apenas uma maneira de acessar tal aplicação, não devemos ignorar a possibilidade de querermos acessar a mesma aplicação com outra interface. Se pensarmos e programarmos de forma a separá-las (aplicação e interface(s)), facilitaremos a inserção de outras interfaces.
Notar: interface aqui não significa apenas uma interface gráfica, com botões e janelas. Um script que envia dados para uma aplicação e recebe dela uma resposta é uma interface tanto quanto uma interface gráfica. Num exemplo mais incomum, poderíamos pensar numa balança de supermercado enviando/recebendo dados para/de uma aplicação, enquanto uma interface web, consultando a mesma aplicação, exibe os dados que foram informados pela balança. Nesse caso, interface web e balança são interfaces para a aplicação.
Faremos uma aplicação de exemplo ainda nesse texto e ilustraremos este conceito escrevendo, para a mesma aplicação, duas interfaces.
Facilitação de implementação
Separar aplicação e interface facilita de modo estrutural a implementação de um projeto de aplicação web. Quisemos fazer um tópico especial para esta vantagem porque é, a nosso ver, muito importante.
Uma maneira de fazer um projeto de software com interface web ("aplicação web") é levantar alguns requisitos e começar a escrever controladores, models, views, até que o problema tenha sido "resolvido". Consideramos que essa abordagem é, sim, válida, se estamos queremos fazer um esboço muito rápido para um problema pequeno ou um projeto de um usuário só. No entanto, se o projeto pode vir minimamente a creser ou precisa de uma estabilidade maior, consideramos que um outro método dá retorno melhor.
Poderíamos descrever esse método melhor como: "primeiro: o model".
O que isso quer dizer é que, até onde possível, devemos implementar primeiro o model, e de forma a ignorar as interfaces que com ele conversarão.
Por exemplo, consideremos uma demanda: "aplicação web que receba uma fórmula de composto químico e responda se ele é orgânico ou não". Começar por criar telas e controladores, juntamente com models, não seria talvez a melhor maneira. Seria melhor criar rotinas que recebem a fórmula e respodem "orgânico: sim ou não". Isso pode ser feito com um programa/classe/software de qualquer; aqui estamos chamando este programa de "model". Ilustraremos concretamente com uma aplicação de exemplo ainda nesse texto.
Só quando este programa estiver feito, pensaremos em controladores ou telas. Ou, escrevendo melhor: não há nada de errado em pensar em telas e controladores antecipadamente, desde que nossos models não sejam modificados por causa de uma dificuldade de interface (algo que talvez deva ser resolvido no controller).
Esta maneira de pensar e programar traz vantagens porque nos concentramos primeiro em resolver o problema (no caso, o problema dos compostos químicos). Quando existirem rotinas pensaremos em como o usuário vai conversar com essas rotinas: aí que entra a interface web (ou outra qualquer).
Para explicitar bem o que queremos mostrar, vamos escrever uma aplicação da forma "errada", com as regras da aplicação estando fortemente ligadas à interface web; depois vamos refatorá-la para que fique com uma boa arquitetura. Faremos uma interface de linha de comando para ilustrar que uma boa arquitetura facilita a tarefa.
Usaremos Perl e o framework web Catalyst para escrever os exemplos, mas mesmo não os conhecendo será possível entender os exemplo com um mínimo de esforço.
Construiremos um programa que, ao receber uma string representando uma substância química, deve responder se ela é orgânica ou não.
$composto = 'CH4'; # metano (CH4) é um composto orgânico
$resposta = checar_organico($composto);
print $resposta; # Imprimirá '1' (true)
Resolução acoplada ao framework web
Notar: nenhum dos códigos escritos aqui foi rodado. O objetivo deles não é estar completo, mas ilustrar conceitos.
Vamos criar a estrutura básica da aplicação web. Essa estrutura é criada automaticamente pelo comando catalyst.pl, que poderemos acessar de um console, depois de instalarmos o módulo Perl Catalyst::Devel. No console, mudaremos o diretório atual para o diretório /tmp com o comando cd /tmp (change directory).
Notar: estamos propositalmente começando errado, ou seja, começando por criar a aplicação web e por escrever código acoplado a ela. Deveríamos começar criando classes para resolver nosso problema, e, só depois, criar uma interface web que conversará com aquelas classes.
$ catalyst.pl QuimicaWeb
Esse comando criará vários diretórios e vários arquivos para nos ajudar a iniciar nosso projeto. Destacaremos dois diretórios importantes, onde ficarão controllers e models. Outros arquivos podemos ignorar.
lib/QuimicaWeb/Controller
lib/QuimicaWeb/Model
Já que estamos implementando da maneira errada, ilustrativamente, devemos criar o arquivo e a classe indicados, que será um model e que conterá dentro de si as rotinas que, depois, colocaremos num módulo separado:
# lib/QuimicaWeb/Model/Utilitarios.pm
QuimicaWeb::Model::Utilitarios
Devemos criar um método em QuimicaWeb::Model::Utilitarios chamado checar_organico que receberá uma string com a descrição de um composto químico e retornará verdadeiro ou falso.
# arquivo /tmp/QuimicaWeb/lib/QuimicaWeb/Model/Utilitarios.pm
package QuimicaWeb::Model::Utilitarios;
sub checar_organico {
# Alguma lógica vai dizer se o composto é ou não orgânico
Agora devemos usar esse model em um controller que criaremos.
# arq: /tmp/QuimicaWeb/lib/QuimicaWeb/Controller/Compostos.pm
package QuimicaWeb::Controller::Compostos;
# O método "checagem" será mapeado a algum endereço como
# algum-host.com.br/compostos/checar
sub checagem :Path('/compostos/checar') Args(0) {
# $composto foi inserido por usuário na interface web
my $model       = $c->model('Utilitarios');
my $eh_organico = $model->checar_organico($composto);
$c->detach('/compostos/organicos') if $eh_organico;
Está concluído nosso primeiro exemplo, que acopla as rotinas de resolução do problema com as rotinas da interface web. Notar que a rotina que faz a resolução do problema (o método QuimicaWeb::Model::Utilitarios::checar_organico) está dentro de uma classe que estende a classe Catalyst::Model, ou seja, esse método está dentro de um model que é parte do framework web.
Resolução desacoplada do framework web
Talvez a dúvida que nos chegue primeiro quando queremos desacoplar código é: onde colocar o código que será desacoplado? Se tiraremos o método checar_organico da classe QuimicaWeb::Model::Utilitarios, para onde então ele deve ir? A resposta é: devemos fazer um módulo (um módulo normal, como qualquer outro) e nele colocar a lógica de resolução do nosso problema.
Por exemplo, podemos fazer:
# arquivo ???/local_padrao_de_modulos_perl/Quimica/Organica.pm
package Quimica::Organica;
Notar que o módulo acima não estende nenhuma classe do namespace Catalyst::. Esse é um módulo comum, que pode ser utilizado por qualquer programa Perl.
Fazendo a classe Quimica::Organica, podemos apagar a classe QuimicaWeb::Model::Utilitarios.
Outra dúvida: agora que desacoplamos, como utilizaremos, dentro da nossa aplicação web Catalyst, o código da classe Quimica::Organica? Ou: como adicionamos uma interface web à classe Quimica::Organica?
Uma das respostas (há mais de uma) é fazer um model que seja apenas uma fina camada entre o framework web e a classe Quimica::Organica. Mostraremos esse método.
# arquivo /tmp/QuimicaWeb/lib/QuimicaWeb/Model/AjudaQuimica.pm
# O nome do arquivo e da classe são arbitrários
package QuimicaWeb::Model::AjudaQuimica;
__PACKAGE__->config(class => 'Quimica::Organica');
Notar a linha __PACKAGE__->config(class => 'Quimica::Organica'). Ela informa que classe esse model retornará quando algum código acessar esse model. Esquematizando:
1. código X acessa model QuimicaWeb::Model::AjudaQuimica
2. QuimicaWeb::Model::AjudaQuimica acessa Quimica::Organica
3. QuimicaWeb::Model::AjudaQuimica retorna Quimica::Organica
para o código X
Feito isso, modificamos nosso controlador:
# Antes, estávamos usando $c->model('Utilitarios')
my $model = $c->model('AjudaQuimica');
if ($eh_organico) {
Notar que só a linha my $model = ... foi modificada. Antes ela chamava o model 'Utilitarios', e agora chama o 'AjudaQuimica'. Como explicado antes, o model 'AjudaQuimica' apenas é uma fina camada entre o framework web e a classe Quimica::Organica. Assim, a variável $model, ao chamar o método checar_organico estará chamando este método na classe Quimica::Organica.
Informação avançada e não importante para nossa discussão agora: o Adaptor instancia a classe externa quando a aplicação web é rodada. Se queremos que a classe externa seja instaciada a cada vez que o model é chamado, devemos usar Catalyst::Model::Factory. Se queremos que a classe externa seja instaciada a cada requisição à aplicação, devemos usar Catalyst::Model::Factory::PerRequest.
Adicionando outra interface
Como modularizamos as rotinas da aplicação e as rotinas da aplicação web, podemos agora facilmente reutilizar o código da aplicação (não-web) dando-lhe outra interface.
Criaremos um script que poderá ser usado num console. Ele receberá o composto como argumento do script.
# arquivo /tmp/interface_quimica.pl
use Quimica::Organica; # Possibilidade que nós usemos o módulo
# Quimica::Organica
my $composto = shift; # Usa o primeiro argumento passado para
# o script no console
my $objeto      = Quimica::Organica->new;
my $eh_organico = $objeto->checar_organico($composto);
if ($eh_organico) { print "É orgânico" }
else              { print "Não é orgânico" }
Podemos usar o script acima no console:
$ perl interface_quimica.pl CH4
Para resolver melhor um problema com uma aplicação web, achamos que é benéfico começar com implementação de rotinas que resolvam o problema, para só então adicionar interfaces a elas. Alguns dos benefícios trazidos por essa prática são foco na resolução do problema, facilidade de implementar testes e facilidade de adicionar outras interfaces.
Na aplicação de exemplo, deveríamos então ter começado pelo módulo Quimica::Organica, e só depois ter criado a aplicação web.